diff --git a/.gitea/ISSUE_TEMPLATE.md b/.gitea/ISSUE_TEMPLATE.md deleted file mode 100644 index f6dedd485..000000000 --- a/.gitea/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve - ---- -You can use the `Preview` tab ^ above to see final rendering of your report. Use `x` in brackets ([x]) to "check" a checkbox. - -If you just have a question, please ask first in the user chatroom in Matrix: [`#gadgetbridge:matrix.org`](https://matrix.to/#/#gadgetbridge:matrix.org) - - -#### Before reporting a bug, please confirm the following: -- [ ] I have read the [wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki), and I didn't find a solution to my problem / an answer to my question. -- [ ] I have searched the [issues](https://codeberg.org/Freeyourgadget/Gadgetbridge/issues), and I didn't find a solution to my problem / an answer to my question. -- [ ] If you upload an image or other content, please make sure you have read and understood the [Codeberg Terms of Use](https://codeberg.org/Codeberg/org/src/branch/main/TermsOfUse.md) - -### Where did you get Gadgetbridge from: -* [ ] F-Droid -* [ ] Gadgetbridge Nightly F-Droid repository -* [ ] Bangle.js Gadgetbridge from the Play Store -* [ ] I built it myself from source code (specify tag / commit) -* [ ] I previously used Gadgetbridge from other sources and then updated to F-Droid version - -### Your Gadgetbridge version is: - -(This can be found in Gadgetbridge - Menu - About: Version and Commit) - -#### Your issue is: -*If possible, please attach [logs](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Log-Files)! that might help identifying the problem.* -*Long logs can be also included but make sure to tuck them into the `details` tag:* - -
- Click to see my log under this fold - -``` -Here go lines of your log. -``` -
- - -#### Your wearable device is: - -*Please specify model and firmware version if possible* - -#### Your Android version/manufacturer flavor is: - - -*New issues about already solved/documented topics could be closed without further comments. Same for too generic or incomplete reports.* - -Please use `Preview` tab above to see final rendering of your report before submitting. diff --git a/.gitea/ISSUE_TEMPLATE/bug_report.md b/.gitea/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index f6dedd485..000000000 --- a/.gitea/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve - ---- -You can use the `Preview` tab ^ above to see final rendering of your report. Use `x` in brackets ([x]) to "check" a checkbox. - -If you just have a question, please ask first in the user chatroom in Matrix: [`#gadgetbridge:matrix.org`](https://matrix.to/#/#gadgetbridge:matrix.org) - - -#### Before reporting a bug, please confirm the following: -- [ ] I have read the [wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki), and I didn't find a solution to my problem / an answer to my question. -- [ ] I have searched the [issues](https://codeberg.org/Freeyourgadget/Gadgetbridge/issues), and I didn't find a solution to my problem / an answer to my question. -- [ ] If you upload an image or other content, please make sure you have read and understood the [Codeberg Terms of Use](https://codeberg.org/Codeberg/org/src/branch/main/TermsOfUse.md) - -### Where did you get Gadgetbridge from: -* [ ] F-Droid -* [ ] Gadgetbridge Nightly F-Droid repository -* [ ] Bangle.js Gadgetbridge from the Play Store -* [ ] I built it myself from source code (specify tag / commit) -* [ ] I previously used Gadgetbridge from other sources and then updated to F-Droid version - -### Your Gadgetbridge version is: - -(This can be found in Gadgetbridge - Menu - About: Version and Commit) - -#### Your issue is: -*If possible, please attach [logs](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Log-Files)! that might help identifying the problem.* -*Long logs can be also included but make sure to tuck them into the `details` tag:* - -
- Click to see my log under this fold - -``` -Here go lines of your log. -``` -
- - -#### Your wearable device is: - -*Please specify model and firmware version if possible* - -#### Your Android version/manufacturer flavor is: - - -*New issues about already solved/documented topics could be closed without further comments. Same for too generic or incomplete reports.* - -Please use `Preview` tab above to see final rendering of your report before submitting. diff --git a/.gitea/ISSUE_TEMPLATE/device_request.md b/.gitea/ISSUE_TEMPLATE/device_request.md deleted file mode 100644 index 1470500df..000000000 --- a/.gitea/ISSUE_TEMPLATE/device_request.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -name: Device request -about: Request for a new device -labels: -- device request ---- - -You are trying to submit a request for a device implementation into Gadgetbridge. We cannot make any promise that the device will be implemented, as adding an implementation for a new device requires either an experienced or a willing to learn developer, ideally with the device at hand. Without that, you may try to submit a device request and see if anyone steps up and implements it and you are basically indicating that you have the device and are willing to help with testing, should anyone try to add this device to Gadgetbridge. - -You can use the `Preview` tab ^ above to see final rendering of your report. Use `x` in brackets ([x]) to "check" a checkbox. - -#### Before proceeding further, please confirm the following: -- [ ] I have read the [wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki), and I didn't find this device mentioned there -- [ ] I have searched the [issues](https://codeberg.org/Freeyourgadget/Gadgetbridge/issues), and I didn't find this device mentioned there -- [ ] Please make sure you have read and understood the [Codeberg Terms of Use](https://codeberg.org/Codeberg/org/src/branch/main/TermsOfUse.md) - -#### Device information - -- Provide device name, manufacturer and similarity to other devices: - - -- Ideally, use an Android Bluetooth scanner app like nRF Connect or BLExplorer and provide screenshots of the scanned device from that app. This provides a name and some available UUIDs, which are needed for implementation. You may want to blur a MAC address for privacy reasons. - - -- Specify model and firmware version if possible: - - -- Please let us know if you have the device and if you are able to provide logs and testing: - - -- If you want to help getting this device supported (and thus speed up the process), please take a look at: - - what you can do as a user: [Support for a new Device](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Support-for-a-new-Device) - - if you are a developer and could maybe even help getting the code ready: [New Device Tutorial](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/New-Device-Tutorial) - -- If possible, please attach [logs](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Log-Files)! which might help to implement the device support into Gadgetbridge. -*Long logs can be also included in the text but make sure to tuck them into the `details` tag below:* - -
- Click to see my log under this fold - -``` -Here go lines of your log. -``` -
- - - - -*New issues about already solved/documented topics could be closed without further comments. Same for too generic or incomplete reports.* - -Please use `Preview` tab above to see final rendering of your report before submitting. diff --git a/.gitea/ISSUE_TEMPLATE/feature_request.md b/.gitea/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 4e92a04aa..000000000 --- a/.gitea/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -name: Feature request -about: Suggest a feature or an idea ---- -You can use the `Preview` tab ^ above to see final rendering of your report. Use `x` in brackets ([x]) to "check" a checkbox. - -If you just have a question, please ask first in the user chatroom in Matrix: [`#gadgetbridge:matrix.org`](https://matrix.to/#/#gadgetbridge:matrix.org) - -#### Before requesting a new feature, please confirm the following: -- [ ] I have read the [wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki), and I didn't find a solution to my problem / an answer to my question. -- [ ] I have searched the [issues](https://codeberg.org/Freeyourgadget/Gadgetbridge/issues), and I didn't find a solution to my problem / an answer to my question. -- [ ] If you upload an image or other content, please make sure you have read and understood the [Codeberg Terms of Use](https://codeberg.org/Codeberg/org/src/branch/main/TermsOfUse.md) - -#### Log files -*If applicable, please attach [logs](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Log-Files)* -*Long logs can be also included in the text but make sure to tuck them into the `details` tag below:* - -
- Click to see my log under this fold - -``` -Here go lines of your log. -``` -
- - -#### Your wearable device is: - -*Please specify model and firmware version if possible* - -#### Your Android version/manufacturer flavor is: - -#### Your Gadgetbridge version is: - - - -*New requests about already solved/documented topics could be closed without further comments. Same for too generic or incomplete reports.* - -Please use `Preview` tab above to see final rendering of your report before submitting. - diff --git a/.gitea/PULL_REQUEST_TEMPLATE.md b/.gitea/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 5b92d9460..000000000 --- a/.gitea/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,14 +0,0 @@ -Thank you for your contribution! 🎉 - -Please make sure that you: - -- Use `git rebase` to bring your branch up to date, do not use a merge commit - for that -- Do not add translations by editing the language variants of strings.xml as - that creates merge conflicts between Codeberg git repo and Weblate git repo, - use Weblate https://hosted.weblate.org/projects/freeyourgadget/gadgetbridge/ -- do not use `e.printStacktrace()`, use slf4j logger or `GB.toast` for logging - as per https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Developer-Documentation#logging -- Please erase these hints from this PR :), thank you! ---- - diff --git a/.gitea/issue_template/bug_report.yml b/.gitea/issue_template/bug_report.yml new file mode 100644 index 000000000..a4c186ae0 --- /dev/null +++ b/.gitea/issue_template/bug_report.yml @@ -0,0 +1,77 @@ +name: Bug report +about: Create a report to help us improve. +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to make Gadgetbridge better! + + If you just have a question, please ask first in the user chatroom in Matrix at [`#gadgetbridge:matrix.org`](https://matrix.to/#/#gadgetbridge:matrix.org) + - type: checkboxes + attributes: + label: Please confirm that; + options: + - label: I have checked the [website](https://gadgetbridge.org), and I didn't find a solution to my problem / an answer to my question. + required: true + - label: I have searched the [issues](https://codeberg.org/Freeyourgadget/Gadgetbridge/issues), and I didn't find a solution to my problem / an answer to my question. + required: true + - label: I have read and understood the [Codeberg Terms of Use](https://codeberg.org/Codeberg/org/src/branch/main/TermsOfUse.md) for images or other type of content that I included here. + required: true + - type: dropdown + id: source + attributes: + label: Where did you get Gadgetbridge from? + options: + - F-Droid + - Gadgetbridge Nightly F-Droid repository + - Bangle.js Gadgetbridge from the Play Store + - I built it myself from source code + - I previously used Gadgetbridge from other sources and then updated to F-Droid version + validations: + required: true + - type: input + id: version + attributes: + label: What is your Gadgetbridge version? + description: | + This can be found in "Menu > About > Version" in Gadgetbridge. + Also include tag / commit SHA if you built Gadgetbridge from the source. + placeholder: e.g. "0.77.0" or "0.77.0-2618adac1" to include commit + validations: + required: true + - type: textarea + id: content + attributes: + label: What happened? + description: > + Please note that new issues about already solved/documented topics + **could be closed without further comments.** Same for too generic or incomplete reports. + placeholder: If you want to include logs, don't include it here. Use the next text field for that. + validations: + required: true + - type: textarea + id: logs + attributes: + label: Do you have logs? + description: > + If possible, please attach [logs](https://gadgetbridge.org/internals/topics/logs/) + that might help identifying the problem. This will be automatically formatted into + code, so no need for backticks. + render: shell + - type: input + id: gadget + attributes: + label: What gadget do you use? + description: > + Please specify model and firmware version if possible. Leave blank if you believe the + issue is not specific to the gadget that you currently use with Gadgetbridge. + placeholder: e.g. ExampleWatch A1 with 0.1 firmware + - type: input + id: android + attributes: + label: What is your Android version/manufacturer flavor? + description: > + Android phone manufacturers may customise the Android source code as they wish, so + if you are using a phone that running a vendor-exclusive system (like MIUI) or if + you use a custom ROM, make sure to also include the name of the OS/ROM. + placeholder: e.g. LineageOS 20 based on Android 13 \ No newline at end of file diff --git a/.gitea/issue_template/config.yml b/.gitea/issue_template/config.yml new file mode 100644 index 000000000..4abd84b80 --- /dev/null +++ b/.gitea/issue_template/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: Ask a question + url: https://matrix.to/#/#gadgetbridge:matrix.org + about: If you just have a question, please ask in the user chatroom in Matrix. + - name: Website + url: https://gadgetbridge.org + about: Visit Gadgetbridge website for FAQ, getting started and common troubleshooting. + - name: See supported gadgets + url: https://gadgetbridge.org/gadgets/ + about: List of all supported gadget vendors and models in Gadgetbridge. \ No newline at end of file diff --git a/.gitea/issue_template/device_request.yml b/.gitea/issue_template/device_request.yml new file mode 100644 index 000000000..724f12054 --- /dev/null +++ b/.gitea/issue_template/device_request.yml @@ -0,0 +1,77 @@ +name: Device request +about: Request for a new device/gadget. +labels: + - device request +body: + - type: markdown + attributes: + value: > + You are trying to submit a request for a device implementation into Gadgetbridge. + **We cannot make any promise that the device will be implemented**, as adding an implementation + for a new device requires either an experienced or a willing to learn developer, + ideally with the device at hand. Without that, you may try to submit a device request + and see if anyone steps up and implements it and you are basically indicating that you + have the device and are willing to help with testing, should anyone try to add + this device to Gadgetbridge. + - type: markdown + attributes: + value: | + Thanks for taking the time to make Gadgetbridge better! + + If you just have a question, please ask first in the user chatroom in Matrix at [`#gadgetbridge:matrix.org`](https://matrix.to/#/#gadgetbridge:matrix.org) + - type: checkboxes + attributes: + label: Please confirm that; + options: + - label: I have checked the [website](https://gadgetbridge.org) and [gadget list](https://gadgetbridge.org/gadgets/), and I didn't find this device mentioned there. + required: true + - label: I have searched the [issues](https://codeberg.org/Freeyourgadget/Gadgetbridge/issues), and I didn't find this device mentioned there. + required: true + - label: I have read and understood the [Codeberg Terms of Use](https://codeberg.org/Codeberg/org/src/branch/main/TermsOfUse.md) for images or other type of content that I included here. + required: true + - type: input + id: gadget + attributes: + label: Which gadget are you requesting support for? + description: Please specify name, manufacturer, model and firmware version if possible. + placeholder: e.g. ExampleWatch A1 with 0.1 firmware + validations: + required: true + - type: input + id: version + attributes: + label: Does this gadget have similarities to other gadgets? + description: > + If the gadget you request support for has similarities to other gadgets that we currently support, + it is more likely for us (still, not guaranteed) to add support for your gadget too. + - type: textarea + id: info + attributes: + label: Device information + description: > + Include all details about the gadget that will be useful to us when implementing a support for this gadget. + + Ideally, use an Android Bluetooth scanner app like nRF Connect or BLExplorer and provide screenshots + of the scanned gadget from that app. This provides a name and some available UUIDs, which are needed + for implementation. You may want to blur MAC addresses for privacy reasons. + placeholder: If you want to include logs, don't include it here. Use the next text field for that. + validations: + required: true + - type: textarea + id: logs + attributes: + label: Do you have logs? + description: > + If possible, please attach [logs](https://gadgetbridge.org/internals/topics/logs/) + which might help to implement the device support into Gadgetbridge. This will be automatically + formatted into code, so no need for backticks. + render: shell + - type: dropdown + id: source + attributes: + label: Do you have the gadget and are you able to help us for testing? + options: + - Yes, I have the gadget and I'm willing to provide further feedback and help testing. + - No, I don't have the gadget or/and I'm not able to help during the development. + validations: + required: true \ No newline at end of file diff --git a/.gitea/issue_template/feature_request.yml b/.gitea/issue_template/feature_request.yml new file mode 100644 index 000000000..cdabcef1a --- /dev/null +++ b/.gitea/issue_template/feature_request.yml @@ -0,0 +1,76 @@ +name: Feature request +about: Suggest a feature or an idea. +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to make Gadgetbridge better! + + If you just have a question, please ask first in the user chatroom in Matrix at [`#gadgetbridge:matrix.org`](https://matrix.to/#/#gadgetbridge:matrix.org) + - type: checkboxes + attributes: + label: Please confirm that; + options: + - label: I have checked the [website](https://gadgetbridge.org), and I didn't find a solution to my problem / an answer to my question. + required: true + - label: I have searched the [issues](https://codeberg.org/Freeyourgadget/Gadgetbridge/issues), and I didn't find a solution to my problem / an answer to my question. + required: true + - label: I have read and understood the [Codeberg Terms of Use](https://codeberg.org/Codeberg/org/src/branch/main/TermsOfUse.md) for images or other type of content that I included here. + required: true + - type: dropdown + id: source + attributes: + label: Where did you get Gadgetbridge from? + options: + - F-Droid + - Gadgetbridge Nightly F-Droid repository + - Bangle.js Gadgetbridge from the Play Store + - I built it myself from source code + - I previously used Gadgetbridge from other sources and then updated to F-Droid version + validations: + required: true + - type: input + id: version + attributes: + label: What is your Gadgetbridge version? + description: | + This can be found in "Menu > About > Version" in Gadgetbridge. + Also include tag / commit SHA if you built Gadgetbridge from the source. + placeholder: e.g. "0.77.0" or "0.77.0-2618adac1" to include commit + validations: + required: true + - type: textarea + id: content + attributes: + label: What is your suggestion/idea? + description: > + Please note that new requests about already solved/documented topics + **could be closed without further comments.** Same for too generic or incomplete reports. + placeholder: If you want to include logs, don't include it here. Use the next text field for that. + validations: + required: true + - type: textarea + id: logs + attributes: + label: Do you have logs? + description: > + If possible, please attach [logs.](https://gadgetbridge.org/internals/topics/logs/) + This will be automatically formatted into code, so no need for backticks. + render: shell + - type: input + id: gadget + attributes: + label: What gadget do you use? + description: > + Please specify model and firmware version if possible. Leave blank if you believe the + issue is not specific to the gadget that you currently use with Gadgetbridge. + placeholder: e.g. ExampleWatch A1 with 0.1 firmware + - type: input + id: android + attributes: + label: What is your Android version/manufacturer flavor? + description: > + Android phone manufacturers may customise the Android source code as they wish, so + if you are using a phone that running a vendor-exclusive system (like MIUI) or if + you use a custom ROM, make sure to also include the name of the OS/ROM. + placeholder: e.g. LineageOS 20 based on Android 13 \ No newline at end of file diff --git a/.gitea/pull_request_template.md b/.gitea/pull_request_template.md new file mode 100644 index 000000000..021ab4b7b --- /dev/null +++ b/.gitea/pull_request_template.md @@ -0,0 +1,16 @@ + \ No newline at end of file diff --git a/.woodpecker/can_master_build.yml b/.woodpecker/can_master_build.yml index dde54fb5c..fb239d34c 100644 --- a/.woodpecker/can_master_build.yml +++ b/.woodpecker/can_master_build.yml @@ -12,5 +12,8 @@ steps: #https://github.com/woodpecker-ci/woodpecker/issues/687 when: - event: - exclude: ['cron', 'deployment'] + # Everything except cron (nightly) and deployment + - event: push + - event: pull_request + - event: tag + - event: manual diff --git a/.woodpecker/nightly.yml b/.woodpecker/nightly.yml index b245e61fc..38894cc10 100644 --- a/.woodpecker/nightly.yml +++ b/.woodpecker/nightly.yml @@ -26,8 +26,8 @@ steps: - if [ -f .do_not_build ]; then return 0; fi # build the apks - echo "$${SIGNING_KEY}" | base64 -d > app/keystore.p12 - - ./gradlew assembleMainNightly -Dnightly_store_file="keystore.p12" -Dnightly_store_password="$${KEYSTOREPASS}" -Dnightly_key_alias="gadgetbridge" -Dnightly_key_password="$${KEYPASS}" - - ./gradlew assembleMainNopebble -Dnightly_store_file="keystore.p12" -Dnightly_store_password="$${KEYSTOREPASS}" -Dnightly_key_alias="gadgetbridge" -Dnightly_key_password="$${KEYPASS}" + - ./gradlew assembleMainlineNightly -Dnightly_store_file="keystore.p12" -Dnightly_store_password="$${KEYSTOREPASS}" -Dnightly_key_alias="gadgetbridge" -Dnightly_key_password="$${KEYPASS}" + - ./gradlew assembleMainlineNopebble -Dnightly_store_file="keystore.p12" -Dnightly_store_password="$${KEYSTOREPASS}" -Dnightly_key_alias="gadgetbridge" -Dnightly_key_password="$${KEYPASS}" - ./gradlew assembleBanglejsNightly -Dnightly_store_file="keystore.p12" -Dnightly_store_password="$${KEYSTOREPASS}" -Dnightly_key_alias="gadgetbridge" -Dnightly_key_password="$${KEYPASS}" secrets: [ signing_key, keystorepass, keypass ] diff --git a/.woodpecker/run_lint.yml b/.woodpecker/run_lint.yml index f78bb032c..83a6501eb 100644 --- a/.woodpecker/run_lint.yml +++ b/.woodpecker/run_lint.yml @@ -12,5 +12,8 @@ steps: #https://github.com/woodpecker-ci/woodpecker/issues/687 when: - event: - exclude: ['cron', 'deployment'] + # Everything except cron (nightly) and deployment + - event: push + - event: pull_request + - event: tag + - event: manual diff --git a/.woodpecker/run_tests.yml b/.woodpecker/run_tests.yml index 2c5a16fdc..cf5717f5a 100644 --- a/.woodpecker/run_tests.yml +++ b/.woodpecker/run_tests.yml @@ -12,5 +12,8 @@ steps: #https://github.com/woodpecker-ci/woodpecker/issues/687 when: - event: - exclude: ['cron', 'deployment'] + # Everything except cron (nightly) and deployment + - event: push + - event: pull_request + - event: tag + - event: manual diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cea7cb0e..602be018f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,35 +1,136 @@ ### Changelog -#### Next release (WIP) +#### 0.79.0 +* Initial support for Honor Magic Watch 2 +* Initial support for Mijia MHO-C303 +* Initial support for Nothing CMF Watch Pro +* Initial support for Sony WI-SP600N +* Experimental support for Redmi Watch 2 +* Experimental support for Xiaomi Smart Band 8 Pro +* Experimental support for Xiaomi Watch S1 Pro +* Experimental support for Xiaomi Watch S1 +* Experimental support for Xiaomi Watch S3 +* Galaxy Buds2 Pro: Fix recognition of some versions +* Huawei Watch GT 2: Fix pairing +* Redmi Smart Band Pro: Fix password digits +* Pebble: Fix app configuration page +* Pebble 2: Fix pairing issue +* PineTime: Fix weather forecast on InfiniTime's new simple weather +* Xiaomi: Fix sleep sometimes extending past the wakeup time +* Xiaomi: Request battery level and charging state periodically +* Xiaomi: Fix sleep stage parsing for some devices +* Zepp OS: Improve device discovery +* Zepp OS: Fix weather not working on some devices +* Zepp OS: Prevent crash when installing large firmware updates +* Fix sport activity summary group order +* Fix reconnection to devices failing occasionally + +#### 0.78.0 +* Initial support for Honor Band 3,4,5,6 +* Initial support for Huawei Band 4, 4 Pro, 6, 7, 3e, 4e +* Initial support for Huawei Talk Band B6 +* Initial support for Huawei Watch GT, GT 2 +* Initial support for Mijia LYWSD03MMC +* Initial support for Nothing Ear (2) +* Initial support for Nothing Ear (Stick) +* Experimental support for Honor Band 7 +* Experimental support for Redmi Watch 2 Lite +* Experimental support for Redmi Smart Band Pro +* Casio GBX100: Add support for snooze alarm +* Fossil/Skagen Hybrids: Update navigationApp to 1.1 +* Huami: Fetch SpO2 on devices that support it +* Pebble: Attempt to fix app configuration webview +* PineTime: Add support for InfiniTime's new simple weather +* PineTime: Fix freeze and reboot when upgrading firmware +* Pixoo: Enable sending images (non-persistent) +* Pixoo: Get and send alarms +* Pixoo: Set custom device name +* Pixoo: support "clap hands to turn off screen" and "sleep after silence" settings +* Xiaomi: Improve activity and workout parsing +* Xiaomi: Improve stability and fix some crashes +* Xiaomi: Improve weather +* Xiaomi: Parse sleep stages +* Add a notifications channel for connection status notifications +* Improve automatic connection to all or previous devices +* Fix devices sometimes staying stuck in a "Connecting" state +* Map some missing Google Maps navigation actions + +#### 0.77.0 * Initial support for Amazfit Balance +* Initial support for Amazfit Active +* Initial support for ColaCao 2021 +* Initial support for ColaCao 2023 * Initial support for Femometer Vinca II * Initial support for Mijia LYWSD02MMC variant * Initial support for Sony Wena 3 +* Experimental support for Divoom Pixoo * Experimental support for Sony WF-1000XM5 +* Experimental support for Amazfit Active Edge +* Experimental support for Mi Band 7 Pro (Xiaomi Smart Band 7 Pro) +* Experimental support for Mi Band 8 (Xiaomi Smart Band 8) +* Experimental support for Mi Watch Lite +* Experimental support for Mi Watch Color Sport +* Experimental support for Redmi Smart Band 2 +* Experimental support for Redmi Watch 3 Active +* Experimental support for Xiaomi Watch S1 Active * Amazfit Band 7: Add alexa menu entries * Amazfit GTR 3 Pro: Fix firmware and watchface upload * Amazfit T-Rex: Fix activity summary parsing +* Amazfit T-Rex Pro: Add activate display on lift sensitivity * AsteroidOS: Add more supported watch models * AsteroidOS: Fix media info * AsteroidOS: Fix notification dismissal * Bangle.js: Add loyalty cards integration with Catima +* Bangle.js: Ensure SMS messages have src field set to "SMS Message" +* Bangle.js: Fix GPS speed +* Bangle.js: Improve handling of chinese characters * Bangle.js: Lower threshold for low battery warning +* Bangle.js: Recover from device initialization failure * Casio GBX100/GBD-200: Fix first connect +* Casio GB5600/6900/STB-1000: Fix pairing +* Casio GDB-200: Fix notification timestamp +* Casio GDB-200: Fixed notification categories and default category +* Casio GDB-200: Allow preview of notification message alongside title +* Casio GDB-200: Fixed find my phone feature +* Intent API: Add debug action for test new function +* Fossil/Skagen Hybrids: Add new navigation app +* Fossil/Skagen Hybrids: Allow configuring call rejection method * Fossil/Skagen Hybrids: Fix some preference crashes on the nightly * Fossil/Skagen Hybrids: Reduce toasts on release builds +* Fossil/Skagen Hybrids: Show device specific settings in more logical order +* Huami: Toggle phone silent mode from band * Message privacy: Add mode Hide only body * Mijia LYWSD02: Add battery +* Mijia LYWSD02: Add low battery notification * Mijia LYWSD02: Set temperature unit +* Mijia LYWSD02: Fix battery drain while connected * PineTime: Display app name for VoIP app calls +* PineTime: Honor Sync time setting on connect +* PineTime: Improve notification handling * PineTime: Reduce weather memory usage * Withings Steel HR: Fix crash when calibrating hands on the nightly +* Zepp OS: Add blood oxygen graph * Zepp OS: Add workout codes for hiking and outdoor swimming +* Zepp OS: Allow disabling app notifications per device +* Zepp OS: Attempt to fix activity fetch operation getting stuck +* Zepp OS: Display swimming activity data * Zepp OS: Fix health settings on older Zepp OS versions * Zepp OS: Fix setting of unknown button press apps +* Zepp OS: Fix sunrise and moon dates being off by local time + UTC offset * Zepp OS: Map hiking, outdoor swimming, climbing and table tennis activity types -* Add transliteration for Latvian and Common Symbols +* Zepp OS: Toggle phone silent mode from band +* Add transliteration for Latvian, Hungarian, Common Symbols +* Allow multiple device actions to be triggered for the same event +* Allow toggling DND through device actions +* Autodetect OsmAnd package name and make it configurable +* Improve ASCII transliterator +* Make GMaps navigation handler follow the "navigation forwarding" setting +* Support selecting enabled navigation apps * Allow ignore notifications from work profile apps +* Display alias in low battery notification +* Fix crash when pairing current device as companion * Fix emoji when a transliterator is enabled +* Fix UV Index and rain probability for some weather apps * Improve device discovery stability and fix freezes * Improve Telegram and COL Reminder notifications * Replace old-style preference switch with Material 3 switch diff --git a/CONTRIBUTORS.rst b/CONTRIBUTORS.rst index 732608c8c..62a05ff0a 100644 --- a/CONTRIBUTORS.rst +++ b/CONTRIBUTORS.rst @@ -24,89 +24,143 @@ * Andreas Shimokawa * Carsten Pfeiffer +* JosÊ Rebelo * Daniele Gobbetti -* Daniel Dakhno * Petr Vaněk * Yaron Shahrabani -* Allan Nordhøy -* Taavi Eomäe +* Daniel Dakhno * 陈少丞 -* Rafael Fontenelle -* JoaĖƒo Paulo Barraca -* Sebastian Kranz +* Arjan Schrijver +* Vincèn PUJOL +* Oğuz Ersen +* Allan Nordhøy +* Ihor Hordiichuk * nautilusx +* Taavi Eomäe +* Gordon Williams +* Rafael Fontenelle +* Michal L +* Sebastian Kranz +* JoaĖƒo Paulo Barraca +* Linerly +* Rex_sa * mamucho * postsorino -* Oğuz Ersen -* FransM +* Manuel-Senpai * Andreas BÃļhler +* FransM * Jonas +* HenRy * Yukai Li * Roi Greenberg -* HenRy +* gallegonovato +* Nikita Epifanov +* kirill blaze +* Óscar FernÃĄndez Díaz +* Jeannette L * Vadim Kaushan * protomors * Cre3per -* Michal L -* JosÊ Rebelo -* Vincèn PUJOL -* Nikita Epifanov +* Davis Mosenkovs +* ssantos * Michael +* glemco * 115ek +* 0que <0que@users.noreply.hosted.weblate.org> +* ĐĄĐ°ŅˆĐ° ПĐĩŅ‚Ņ€ĐžĐ˛Đ¸Ņ› * naofum -* Gordon Williams +* My Random Thoughts +* Damien 'Psolyca' Gaignon +* 0eoc <0eoc@users.noreply.hosted.weblate.org> * mesnevi -* Jeannette L +* Kintu * youzhiran <2668760098@qq.com> * mueller-ma * ivanovlev * Tijl Schepens -* ssantos * Sophanimus * Pavel Elagin +* NekoBox +* MPeter <> +* MrYoranimo * mondstern * HadriÃĄn Candela +* Ács ZoltÃĄn * Zhong Jianxin -* Kintu +* Milo Ivir +* Gabriele Monaco +* foxstidious +* Andy Yang * Abdullah Manaz +* Richard de Boer * mkusnierz <> * Julien Pivotto +* tomechio * Steffen Liebergeld +* Skrripy +* Petr Kadlec +* Pavel * Lem Dulfo +* Dmitriy Bogdanov +* Olexandr Nesterenko * Nevena Mircheva +* musover * Matthieu Baerts -* J. Lavoie * Felix Konstantin Maurer -* Andy Yang +* Axus Wizix +* Xtremo3 * Utsob Roy * taras3333 * Sergey Trofimov +* Sebastian Krey +* Noodlez * M. Hadi +* Martin Boonk +* Lukas +* Ganblejs +* Deixondit +* akasaka / Genjitsu Labs * Szylu * Robert Barat -* Pavel +* Reza Almanda * Mario * ksiwczynski * JohnnySun * Gilles Émilien MOREL -* Deixondit +* firekonstantin +* bruh * Uwe Hermann +* Patric Gruber * opavlov -* Olexandr Nesterenko +* Michalis +* Mario Rossi +* ifurther * Edoardo Rosa -* Dmitriy Bogdanov +* d * BoÅŧydar * Alberto +* AiLab * zsolt3991 +* winver * Vladislav Serkov * Vebryn +* uli * Ted Stein +* sinore +* Shimon +* Reiner Herrmann * NicoBuntu +* Nee Sorry +* Marc Nause * Louis-Marie Croisez +* KryÅĄtof ČernÃŊ +* Johannes Krude * Jean-François Greffier +* Hasan Ammar * Giuseppe Caliendo * Gergely Peidl * Fabio Parri +* Evo * Emre * Elwood * Dmitry Markin @@ -118,153 +172,236 @@ * 0nse <0nse@users.noreply.github.com> * МаĐēŅĐ¸Đŧ Đ¯ĐēиĐŧŅ‡ŅƒĐē * Ye Wint Htut Kyaw +* xaos +* Thomas +* TheScientistPT * SnowCat +* Sergio Varela * Sebastian Obrusiewicz +* Sebastian Espinosa +* Robbert Gurdeep Singh * Rimas RaguliÅĢnas +* mvn23 * Minori Hiraoka (미노ëĻŦ) +* MASVA * masakoodaa * Marius Cornescu -* Mario Rossi +* mantas-p * Lukas Veneziano * LL +* LizardWithHat +* Lesur Frederic * leela <53352@protonmail.com> +* kukuruzka * Kompact +* Kalle * K0L0B0G * Johann C. Rode +* jimman2003 * jfgreffier * Jasper +* ITCactus +* illis * Francesco Marinucci +* FintasticMan +* Doma Gergő * Dikay900 * Denis * Christian Fischer +* Benjamin Swartley * Asbesbopispa -* AiLab * Adolfo Jayme Barrientos * 6arms1leg +* Your Name * XqweX * walkjivefly * WaldiS * Vytenis * Vladislav Glinsky * vishnu -* Thomas -* Sebastian Espinosa +* Vianney le ClÊment de Saint-Marcq +* Toby Murray +* thyttan <6uuxstm66@mozmail.com> +* Thorsten +* Stephan Lachnit +* Sebastian Reichel * Saul Nunez * Rui Mendes +* roolx +* rarder44 +* rany * Ranved Sticon * Rajesh Kumbhakar +* Ptilopsis Leucotis * petronovak -* Petr Kadlec * Pascal * odavo32nof +* octospacc * NotAFIle * Normano64 +* Nikolay Korotkiy * Nick Spacek -* Nee Sorry +* Nekromanser * Nathan +* narektor * MyTimeKill <26295589+MyTimeKill@users.noreply.github.com> * MolnÃĄr BarnabÃĄs * Moarc * Michal Novotny +* maxvel +* Maxime Reyrolle * Mattias MÃŧnster * Mattherix * Martin * marco.altomonte -* LizardWithHat * Le Poisson Libre +* Krzysztof Marcinek * krzys_h * Konrad Iturbe +* Kamalei Zestri <38802353+KamaleiZestri@users.noreply.github.com> +* Joel Beckmeyer * JesÃēs * JesÃēs F * Irul -* ifurther +* Igor Polyakov * homocomputeris +* Grzegorz +* GeekosaurusR3x * Francesco Franchina * fparri +* Fabien Brachere * exit-failure +* Ertu (Er2, Err) +* Er2 * Dreamwalker +* DAWID * Dario Lopez-Kästen * Da Pa * DanialHanif * Cristian Alfano * criogenic * chabotsi +* bowornsin * Avamander +* Artem * AnthonyDiGirolamo * Anonymous * Andreas Kromke +* Alex +* Albert * Ainārs +* ؚبداŲ„ØąØĻŲˆŲ ؚابدی +* ЕĐŗĐžŅ€ ЕŅ€ĐŧĐ°ĐēОв * Ⲇⲁⲛⲓ ÎĻi -* Your Name +* Yusuf Cihan +* yk * Yar * xzovy * xphnx +* XosÊ M +* Xeoy * Xavier RENE-CORAIL -* xaos +* x29a * w2q * Vitaliy Shuruta * veecue * Unixware +* TylerWilliamson * Triet Pham +* Traladarer * Tomer Rosenfeld * Tomas Radej -* Toby Murray * t-m-w * tiparega <11555126+tiparega@users.noreply.github.com> * TinfoilSubmarine +* Tim +* thirschbuechler * Thiago Rodrigues +* thermatk +* theghostofheathledger +* Temuri Doghonadze * Tarik Sekmen * Szymon Tomasz Stefanek * szilardx <15869670+szilardx@users.noreply.github.com> * Swann Martinet +* Stefan Bora * Stan Gomin +* ssilverr +* Sky233ml * SinMan +* Simon Sievert * Sergio Lopez +* Sergey Vasilyev +* sedy89 +* Sebastian Nilsson * S Dantas * Santiago BenalcÃĄzar * Samuel Carvalho de AraÃējo * Sami Alaoui <4ndroidgeek@gmail.com> +* Saman rsh +* Salif Mehmed +* SalavatR * Roxystar * Roman Plevka +* rom4nik +* Robin Davidsson +* Roberto P. Rubio * rober * Rivo Zängov * rimasx +* rikka356 * Richard Finegold * Retew * redking +* Ray +* RandomItalianGuy +* Raghd Hamzeh +* Quang Ngô * Quallenauge * Q-er <9142398+Q-er@users.noreply.github.com> +* pommes +* pishite * Perflyst * Pavel Motyrev * Pauli Salmenrinne * pangwalla * Pander +* ozkanpakdil +* opcode * Ondřej SedlÃĄÄek * Olivier Bloch +* Oleg Vasilev +* Oleg * Nur Aiman Fadel * Nikolai Sinyov * NicolÃ˛ Balzarotti * Nephiel +* Nathan Philipp Bo Seddig * Natanael Arndt * Nabil BENDAFI +* myxor +* Morten Rieger Hannemose * Mirko Covizzi -* Milo Ivir +* Milan Å alka * Mike van Rossum +* mika laka * Michal Novak +* Michael Wiesinger * michaelneu -* Lesur Frederic +* MedusasSphinx * McSym28 * MaxL * maxirnilian * Maxim Baz +* Mave95 * Matej Drobnič * Marvin D * Martin Piatka +* Martin.JM <> * Margreet * Marc Schlaich * Marco Alberto Diosdado Nava * Marco A <35718078+TomasCartman@users.noreply.github.com> -* Marc Nause * Marc Laporte * Marcin * Marcel pl (m4rcel) @@ -274,87 +411,142 @@ * magimel.francois * Maciej Kuśnierz <> * m4sk1n +* LukasEdl +* LuK1337 * Luiz Felipe das Neves Lopes * Luis zas +* Ludovic Jozeau * luca sain * lucanomax +* Liao junchao +* Leon Omelan * Leonardo Amaral * Leo bonilla +* LeJun * Lejun * lazarosfs +* Lars Vogdt * ladbsoft <30509719+ladbsoft@users.noreply.github.com> +* Kyaw Min Khant +* KrisztiÃĄn GÃĄncs <990024@gmail.com> * Kristjan Räts +* KornÊl Schmidt +* kirk1984 +* kieranc001 * kevlarcade * Kevin Richter +* Kevin MacMartin * keeshii * Kaz Wolfe * Kasha * kalaee +* Julien Winning * Julian Lam * jugendhacker * Joseph Kim * jonnsoft <> * Johannes Tysiak -* Joan Perals +* Jochen S +* joaquim.org +* jhey * JF +* Jean-François Milants * jcrode <46062294+jcrode@users.noreply.github.com> * Jan Lolek * Jakub Jelínek +* Jacque Fresco * Izzy * iwonder * Ivan -* Igor Polyakov +* InternalErrorX * HÃŧseyin Aslan +* Hugel * hr-sales * Hirnchirurg -* Hasan Ammar +* Hen Ry +* HelloCodeberg * HardLight * Hanhan Husna +* halemmerich * hackoder +* Gustavo Ramires +* gsbhat <> * Grzegorz Dznsk +* Golbinex <2061409-Golbinex@users.noreply.gitlab.com> +* gnufella +* gnu-ewm * Gleb Chekushin * Giuseppe * GideÃŖo Gomes Ferreira +* gfwilliams * GabO * Gabe Schrecker * freezed-or-frozen * Frank Slezak +* Frank Ertl * Florian Beuscher +* Fabian Hof * Étienne Deparis * EstÊbastien Robespi +* Ernst +* Enrico Brambilla * Edoardo Tronconi * Dougal19 <4662351+Dougal19@users.noreply.github.com> +* Donato * Dmytro Bielik +* djurik * DerFetzer +* Dean <3114661520@qq.com> * Deactivated Account -* Davis Mosenkovs +* David GirÃŗn +* Davide Corradini +* Daniel Thompson * Daniel Hauck +* Dam BOND +* ė´ė •íŦ +* Dachi G +* C * cokecodecock +* CodeSpoof * C O * clach04 * Chris Perelstein +* chklump +* CÊdric Bellegarde * Carlos Ferreira * C0rn3j * ButterflyOfFire * bucala * boun +* BobIsMyManager +* Bilel MEDIMEGH * Benjamin Kahlau +* Ben +* beardhatcode * batataspt@gmail.com * atkyritsis -* Artem +* Ascense +* Aprilhoomie * apre +* Ann Test * Aniruddha Adhikary * angelpup +* Anemograph * Andrzej Surowiec +* Andrew Watkins * andrewlytvyn * AndrewH <36428679+andrewheadricke@users.noreply.github.com> * andre +* Andrea Lepori * Allen B <28495335+Allen-B1@users.noreply.github.com> +* Alicia Hormann * Alfeu Lucas Guedes dos Santos -* Alex * Alexey Afanasev * Alexandra Sevostyanova +* Aidan Crane * aerowolf +* Adam BÃŧchner +* a b <65567823+abb128@users.noreply.github.com> And all the former Transifex translators, who cannot be listed automatically. diff --git a/FEATURES.md b/FEATURES.md deleted file mode 100644 index 636664a4c..000000000 --- a/FEATURES.md +++ /dev/null @@ -1,35 +0,0 @@ -## Feature Matrix - -| | Pebble OG | Pebble Time/2 | Mi Band | Mi Band 2 | Mi Band 3 | Mi Band 4/5 | Amazfit Bip | Amazfit Cor | -|-----------------------------------| ----------|---------------|---------|-----------|-----------|-------------|-------------|-------------| -|Calls Notification | YES | YES | YES | YES | YES | YES | YES | YES | -|Reject Calls | YES | YES | NO | NO | YES | YES | YES | YES | -|Accept Calls | NO(2) | NO(2) | NO | NO | NO | NO | NO | NO | -|Generic Notification | YES | YES | YES | YES | YES | YES | YES | YES | -|Dismiss Notifications on Phone | YES | YES | NO | NO | NO | NO | NO | NO | -|Predefined Replies | YES | YES | NO | NO | NO | NO | NO | NO | -|Voice Replies | N/A | NO(3) | N/A | N/A | N/A | N/A | N/A | N/A | -|Calendar Sync | YES | YES | NO | NO | NO | NO | NO(3) | NO | -|Configure alarms from Gadgetbridge | NO | NO | YES | YES | YES | YES(1) | YES | YES | -|Smart alarms | NO(1) | YES | YES | NO | NO | NO | NO | NO | -|Weather | NO(1) | YES | NO | NO | YES | YES | YES | YES | -|Activity Tracking | NO(1) | YES | YES | YES | YES | YES | YES | YES | -|GPS tracks import | NO | NO | NO | NO | NO | NO | YES | NO | -|Sleep Tracking | NO(1) | YES | YES | YES | YES | YES | YES | YES | -|HR Tracking | N/A | YES | YES | YES | YES | YES | YES | YES | -|Realtime Activity Tracking | NO | NO | YES | YES | YES | YES | YES | YES | -|Music Control | YES | YES | NO | NO | NO | YES | NO | YES | -|Watchapp/face Installation | YES | YES | NO | NO | NO | YES | YES | YES | -|Firmware Installation | YES | YES | YES | YES | YES | YES | YES | YES | -|Taking Screenshots | YES | YES | NO | NO | NO | NO | NO | NO | -|Support Android Companion Apps | YES | YES | NO | NO | NO | NO | NO | NO | - -(1) Possible via 3rd Party Watchapp -(2) Theoretically possible (works on iOS, would need lot of work) -(3) Possible but not implemented yet - - -### Notes about Pebble Firmware >=3.0 - -* Gadgetbridge will keep track of installed watchfaces, but if the Pebble is used with another phone or another app, the information displayed in the app manager can get out of sync since it is impossible to query Firmware >= 3.x for installed apps/watchfaces. - diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index a08bfeec4..3fcec24ef 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -45,7 +45,7 @@ public class GBDaoGenerator { public static void main(String[] args) throws Exception { - final Schema schema = new Schema(62, MAIN_PACKAGE + ".entities"); + final Schema schema = new Schema(70, MAIN_PACKAGE + ".entities"); Entity userAttributes = addUserAttributes(schema); Entity user = addUserInfo(schema, userAttributes); @@ -70,6 +70,18 @@ public class GBDaoGenerator { addHuamiHeartRateRestingSample(schema, user, device); addHuamiPaiSample(schema, user, device); addHuamiSleepRespiratoryRateSample(schema, user, device); + addXiaomiActivitySample(schema, user, device); + addXiaomiSleepTimeSamples(schema, user, device); + addXiaomiSleepStageSamples(schema, user, device); + addXiaomiManualSamples(schema, user, device); + addXiaomiDailySummarySamples(schema, user, device); + addCmfActivitySample(schema, user, device); + addCmfStressSample(schema, user, device); + addCmfSpo2Sample(schema, user, device); + addCmfSleepSessionSample(schema, user, device); + addCmfSleepStageSample(schema, user, device); + addCmfHeartRateSample(schema, user, device); + addCmfWorkoutGpsSample(schema, user, device); addPebbleHealthActivitySample(schema, user, device); addPebbleHealthActivityKindOverlay(schema, user, device); addPebbleMisfitActivitySample(schema, user, device); @@ -105,6 +117,12 @@ public class GBDaoGenerator { addWena3StressSample(schema, user, device); addFemometerVinca2TemperatureSample(schema, user, device); + addHuaweiActivitySample(schema, user, device); + + Entity huaweiWorkoutSummary = addHuaweiWorkoutSummarySample(schema, user, device); + addHuaweiWorkoutDataSample(schema, user, device, huaweiWorkoutSummary); + addHuaweiWorkoutPaceSample(schema, user, device, huaweiWorkoutSummary); + addCalendarSyncState(schema, device); addAlarms(schema, user, device); addReminders(schema, user, device); @@ -264,7 +282,7 @@ public class GBDaoGenerator { private static Entity addHuamiStressSample(Schema schema, Entity user, Entity device) { Entity stressSample = addEntity(schema, "HuamiStressSample"); addCommonTimeSampleProperties("AbstractStressSample", stressSample, user, device); - stressSample.addIntProperty("typeNum").notNull().codeBeforeGetterAndSetter(OVERRIDE); + stressSample.addIntProperty("typeNum").notNull().codeBeforeGetter(OVERRIDE); stressSample.addIntProperty("stress").notNull().codeBeforeGetter(OVERRIDE); return stressSample; } @@ -272,7 +290,7 @@ public class GBDaoGenerator { private static Entity addHuamiSpo2Sample(Schema schema, Entity user, Entity device) { Entity spo2sample = addEntity(schema, "HuamiSpo2Sample"); addCommonTimeSampleProperties("AbstractSpo2Sample", spo2sample, user, device); - spo2sample.addIntProperty("typeNum").notNull().codeBeforeGetterAndSetter(OVERRIDE); + spo2sample.addIntProperty("typeNum").notNull().codeBeforeGetter(OVERRIDE); spo2sample.addIntProperty("spo2").notNull().codeBeforeGetter(OVERRIDE); return spo2sample; } @@ -324,6 +342,136 @@ public class GBDaoGenerator { return sleepRespiratoryRateSample; } + private static Entity addXiaomiActivitySample(Schema schema, Entity user, Entity device) { + Entity activitySample = addEntity(schema, "XiaomiActivitySample"); + addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device); + activitySample.implementsSerializable(); + activitySample.addIntProperty(SAMPLE_RAW_INTENSITY).notNull().codeBeforeGetterAndSetter(OVERRIDE); + activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE); + activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE); + addHeartRateProperties(activitySample); + activitySample.addIntProperty("stress"); + activitySample.addIntProperty("spo2"); + return activitySample; + } + + private static Entity addXiaomiSleepTimeSamples(Schema schema, Entity user, Entity device) { + Entity sample = addEntity(schema, "XiaomiSleepTimeSample"); + addCommonTimeSampleProperties("AbstractTimeSample", sample, user, device); + sample.addLongProperty("wakeupTime"); + sample.addBooleanProperty("isAwake"); + sample.addIntProperty("totalDuration"); + sample.addIntProperty("deepSleepDuration"); + sample.addIntProperty("lightSleepDuration"); + sample.addIntProperty("remSleepDuration"); + sample.addIntProperty("awakeDuration"); + return sample; + } + + private static Entity addXiaomiSleepStageSamples(Schema schema, Entity user, Entity device) { + Entity sample = addEntity(schema, "XiaomiSleepStageSample"); + addCommonTimeSampleProperties("AbstractTimeSample", sample, user, device); + sample.addIntProperty("stage"); + return sample; + } + + private static Entity addXiaomiManualSamples(Schema schema, Entity user, Entity device) { + Entity sample = addEntity(schema, "XiaomiManualSample"); + addCommonTimeSampleProperties("AbstractTimeSample", sample, user, device); + sample.addIntProperty("type"); + sample.addIntProperty("value"); + return sample; + } + + private static Entity addXiaomiDailySummarySamples(Schema schema, Entity user, Entity device) { + Entity sample = addEntity(schema, "XiaomiDailySummarySample"); + addCommonTimeSampleProperties("AbstractTimeSample", sample, user, device); + sample.addIntProperty("timezone"); + sample.addIntProperty("steps"); + sample.addIntProperty("hrResting"); + sample.addIntProperty("hrMax"); + sample.addIntProperty("hrMaxTs"); + sample.addIntProperty("hrMin"); + sample.addIntProperty("hrMinTs"); + sample.addIntProperty("hrAvg"); + sample.addIntProperty("stressAvg"); + sample.addIntProperty("stressMax"); + sample.addIntProperty("stressMin"); + sample.addIntProperty("standing"); + sample.addIntProperty("calories"); + sample.addIntProperty("spo2Max"); + sample.addIntProperty("spo2MaxTs"); + sample.addIntProperty("spo2Min"); + sample.addIntProperty("spo2MinTs"); + sample.addIntProperty("spo2Avg"); + sample.addIntProperty("trainingLoadDay"); + sample.addIntProperty("trainingLoadWeek"); + sample.addIntProperty("trainingLoadLevel"); + sample.addIntProperty("vitalityIncreaseLight"); + sample.addIntProperty("vitalityIncreaseModerate"); + sample.addIntProperty("vitalityIncreaseHigh"); + sample.addIntProperty("vitalityCurrent"); + return sample; + } + + private static Entity addCmfActivitySample(Schema schema, Entity user, Entity device) { + Entity activitySample = addEntity(schema, "CmfActivitySample"); + addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device); + activitySample.implementsSerializable(); + activitySample.addIntProperty(SAMPLE_RAW_INTENSITY).notNull().codeBeforeGetterAndSetter(OVERRIDE); + activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE); + activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE); + addHeartRateProperties(activitySample); + activitySample.addIntProperty("distance"); + activitySample.addIntProperty("calories"); + return activitySample; + } + + private static Entity addCmfStressSample(Schema schema, Entity user, Entity device) { + Entity stressSample = addEntity(schema, "CmfStressSample"); + addCommonTimeSampleProperties("AbstractStressSample", stressSample, user, device); + stressSample.addIntProperty("stress").notNull().codeBeforeGetter(OVERRIDE); + return stressSample; + } + + private static Entity addCmfSpo2Sample(Schema schema, Entity user, Entity device) { + Entity spo2sample = addEntity(schema, "CmfSpo2Sample"); + addCommonTimeSampleProperties("AbstractSpo2Sample", spo2sample, user, device); + spo2sample.addIntProperty("spo2").notNull().codeBeforeGetter(OVERRIDE); + return spo2sample; + } + + private static Entity addCmfSleepSessionSample(Schema schema, Entity user, Entity device) { + Entity sleepSessionSample = addEntity(schema, "CmfSleepSessionSample"); + addCommonTimeSampleProperties("AbstractTimeSample", sleepSessionSample, user, device); + sleepSessionSample.addLongProperty("wakeupTime"); + sleepSessionSample.addByteArrayProperty("metadata"); + return sleepSessionSample; + } + + private static Entity addCmfSleepStageSample(Schema schema, Entity user, Entity device) { + Entity sleepStageSample = addEntity(schema, "CmfSleepStageSample"); + addCommonTimeSampleProperties("AbstractTimeSample", sleepStageSample, user, device); + sleepStageSample.addIntProperty("duration").notNull(); + sleepStageSample.addIntProperty("stage").notNull(); + return sleepStageSample; + } + + private static Entity addCmfHeartRateSample(Schema schema, Entity user, Entity device) { + Entity heartRateSample = addEntity(schema, "CmfHeartRateSample"); + addCommonTimeSampleProperties("AbstractHeartRateSample", heartRateSample, user, device); + heartRateSample.addIntProperty(SAMPLE_HEART_RATE).notNull().codeBeforeGetter(OVERRIDE); + return heartRateSample; + } + + private static Entity addCmfWorkoutGpsSample(Schema schema, Entity user, Entity device) { + Entity sample = addEntity(schema, "CmfWorkoutGpsSample"); + addCommonTimeSampleProperties("AbstractTimeSample", sample, user, device); + sample.addIntProperty("latitude"); + sample.addIntProperty("longitude"); + return sample; + } + private static void addHeartRateProperties(Entity activitySample) { activitySample.addIntProperty(SAMPLE_HEART_RATE).notNull().codeBeforeGetterAndSetter(OVERRIDE); } @@ -862,6 +1010,7 @@ public class GBDaoGenerator { return activitySample; } + private static Entity addWithingsSteelHRActivitySample(Schema schema, Entity user, Entity device) { Entity activitySample = addEntity(schema, "WithingsSteelHRActivitySample"); activitySample.implementsSerializable(); @@ -897,7 +1046,7 @@ public class GBDaoGenerator { private static Entity addWena3StressSample(Schema schema, Entity user, Entity device) { Entity stressSample = addEntity(schema, "Wena3StressSample"); addCommonTimeSampleProperties("AbstractStressSample", stressSample, user, device); - stressSample.addIntProperty("typeNum").notNull().codeBeforeGetterAndSetter(OVERRIDE); + stressSample.addIntProperty("typeNum").notNull().codeBeforeGetter(OVERRIDE); stressSample.addIntProperty("stress").notNull().codeBeforeGetter(OVERRIDE); return stressSample; } @@ -943,6 +1092,99 @@ public class GBDaoGenerator { return perAppSetting; } + private static Entity addHuaweiActivitySample(Schema schema, Entity user, Entity device) { + Entity activitySample = addEntity(schema, "HuaweiActivitySample"); + addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device); + activitySample.addIntProperty("otherTimestamp").notNull().primaryKey(); + activitySample.addByteProperty("source").notNull().primaryKey(); + activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE); + activitySample.addIntProperty(SAMPLE_RAW_INTENSITY).notNull().codeBeforeGetterAndSetter(OVERRIDE); + activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE); + activitySample.addIntProperty("calories").notNull(); + activitySample.addIntProperty("distance").notNull(); + activitySample.addIntProperty("spo").notNull(); + activitySample.addIntProperty("heartRate").notNull(); + return activitySample; + } + + private static Entity addHuaweiWorkoutSummarySample(Schema schema, Entity user, Entity device) { + Entity workoutSummary = addEntity(schema, "HuaweiWorkoutSummarySample"); + + workoutSummary.setJavaDoc("Contains Huawei Workout Summary samples (one per workout)"); + + workoutSummary.addLongProperty("workoutId").primaryKey().autoincrement(); + + Property deviceId = workoutSummary.addLongProperty("deviceId").notNull().getProperty(); + workoutSummary.addToOne(device, deviceId); + Property userId = workoutSummary.addLongProperty("userId").notNull().getProperty(); + workoutSummary.addToOne(user, userId); + + workoutSummary.addShortProperty("workoutNumber").notNull(); + workoutSummary.addByteProperty("status").notNull(); + workoutSummary.addIntProperty("startTimestamp").notNull(); + workoutSummary.addIntProperty("endTimestamp").notNull(); + workoutSummary.addIntProperty("calories").notNull(); + workoutSummary.addIntProperty("distance").notNull(); + workoutSummary.addIntProperty("stepCount").notNull(); + workoutSummary.addIntProperty("totalTime").notNull(); + workoutSummary.addIntProperty("duration").notNull(); + workoutSummary.addByteProperty("type").notNull(); + workoutSummary.addShortProperty("strokes").notNull(); + workoutSummary.addShortProperty("avgStrokeRate").notNull(); + workoutSummary.addShortProperty("poolLength").notNull(); + workoutSummary.addShortProperty("laps").notNull(); + workoutSummary.addShortProperty("avgSwolf").notNull(); + + workoutSummary.addByteArrayProperty("rawData"); + + return workoutSummary; + } + + private static Entity addHuaweiWorkoutDataSample(Schema schema, Entity user, Entity device, Entity summaryEntity) { + Entity workoutDataSample = addEntity(schema, "HuaweiWorkoutDataSample"); + + workoutDataSample.setJavaDoc("Contains Huawei Workout data samples (multiple per workout)"); + + Property id = workoutDataSample.addLongProperty("workoutId").primaryKey().notNull().getProperty(); + workoutDataSample.addToOne(summaryEntity, id); + + workoutDataSample.addIntProperty("timestamp").notNull().primaryKey(); + workoutDataSample.addByteProperty("heartRate").notNull(); + workoutDataSample.addShortProperty("speed").notNull(); + workoutDataSample.addByteProperty("stepRate").notNull(); + workoutDataSample.addShortProperty("cadence").notNull(); + workoutDataSample.addShortProperty("stepLength").notNull(); + workoutDataSample.addShortProperty("groundContactTime").notNull(); + workoutDataSample.addByteProperty("impact").notNull(); + workoutDataSample.addShortProperty("swingAngle").notNull(); + workoutDataSample.addByteProperty("foreFootLanding").notNull(); + workoutDataSample.addByteProperty("midFootLanding").notNull(); + workoutDataSample.addByteProperty("backFootLanding").notNull(); + workoutDataSample.addByteProperty("eversionAngle").notNull(); + workoutDataSample.addByteProperty("swolf").notNull(); + workoutDataSample.addShortProperty("strokeRate").notNull(); + + workoutDataSample.addByteArrayProperty("dataErrorHex"); + + return workoutDataSample; + } + + private static Entity addHuaweiWorkoutPaceSample(Schema schema, Entity user, Entity device, Entity summaryEntity) { + Entity workoutPaceSample = addEntity(schema, "HuaweiWorkoutPaceSample"); + + workoutPaceSample.setJavaDoc("Contains Huawei Workout pace data samples (one per workout)"); + + Property id = workoutPaceSample.addLongProperty("workoutId").primaryKey().notNull().getProperty(); + workoutPaceSample.addToOne(summaryEntity, id); + + workoutPaceSample.addIntProperty("distance").notNull().primaryKey(); + workoutPaceSample.addByteProperty("type").notNull().primaryKey(); + workoutPaceSample.addIntProperty("pace").notNull(); + workoutPaceSample.addIntProperty("correction").notNull(); + + return workoutPaceSample; + } + private static void addTemperatureProperties(Entity activitySample) { activitySample.addFloatProperty(SAMPLE_TEMPERATURE).notNull().codeBeforeGetter(OVERRIDE); activitySample.addIntProperty(SAMPLE_TEMPERATURE_TYPE).notNull().codeBeforeGetter(OVERRIDE); @@ -954,5 +1196,4 @@ public class GBDaoGenerator { addTemperatureProperties(sample); return sample; } - } diff --git a/README.md b/README.md index 8582f4561..a4d99b2cd 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Pebble, Mi Band, Amazfit Bip and HPlus device (and more) without the vendor's cl and without the need to create an account and transmit any of your data to the vendor's servers. -[Homepage](https://gadgetbridge.org) - [Blog](https://blog.freeyourgadget.org) - [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki) - Mastodon +[Homepage](https://gadgetbridge.org) - [Blog](https://blog.freeyourgadget.org) - Mastodon [![Donate](https://liberapay.com/assets/widgets/donate.svg)](https://liberapay.com/Gadgetbridge/donate) @@ -21,121 +21,25 @@ vendor's servers. ## Code Licenses -* Gadgetbrige is licensed under the AGPLv3 +* Gadgetbridge is licensed under the AGPLv3 * Files in app/src/main/java/net/osmand/ and app/src/main/aidl/net/osmand/ are licensed under the GPLv3 by OsmAnd BV - +* Files in app/src/main/java/org/bouncycastle are licensed under the MIT license by The Legion of the Bouncy Castle Inc. ## Download [Get it on F-Droid](https://f-droid.org/app/nodomain.freeyourgadget.gadgetbridge) -[List of changes](https://codeberg.org/Freeyourgadget/Gadgetbridge/src/master/CHANGELOG.md) +- [Nightly releases](https://freeyourgadget.codeberg.page/fdroid/repo?fingerprint=CD381ECCC465AB324E21BCC335895615E07E70EE11E9FD1DF3C020C5194F00B2) + - Nightly releases are updated more frequently and may be less stable than standard releases, and they are distributed by our F-Droid repository unlike standard releases. +- [List of changes](https://codeberg.org/Freeyourgadget/Gadgetbridge/src/master/CHANGELOG.md) ## Supported Devices -**(WARNING: Some of them WIP and some of them without maintainer)** -- Amazfit - - [Amazfit Active](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Active), [Amazfit Active Edge](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Active-Edge) [**\[!\]**](#special-pairing-procedures) - - [Balance](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Balance) [**\[!\]**](#special-pairing-procedures) - - [Band 5](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Band-5), [Band 7](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Band-7) [**\[!\]**](#special-pairing-procedures) - - [Bip](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Bip) - - [Bip Lite](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Bip-Lite), [Bip S](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Bip-S), [Bip U, Bip U Pro](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Bip-U) [**\[!\]**](#special-pairing-procedures) - - [Bip 3 Pro](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Bip-3-Pro) [**\[!\]**](#special-pairing-procedures) - - [Bip 5 (experimental)](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Bip-5) [**\[!\]**](#special-pairing-procedures) - - [Cor](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Cor), [Cor 2](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Cor-2) - - [Falcon (experimental)](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Falcon) [**\[!\]**](#special-pairing-procedures) - - [GTR](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-GTR), [GTR 2/2e](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-GTR), [GTR 3](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-GTR-3), [GTR 3 Pro](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-GTR-3-Pro), [GTR 4](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-GTR-4) [**\[!\]**](#special-pairing-procedures) - - [GTR Mini](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-GTR-Mini) [**\[!\]**](#special-pairing-procedures) - - [GTS](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-GTS), [GTS 2/2e](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-GTS), [GTS 3](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-GTS-3), [GTS 4](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-GTS-4), [GTS 4 Mini](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-GTS-4-Mini) [**\[!\]**](#special-pairing-procedures) - - [Neo](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Neo) [**\[!\]**](#special-pairing-procedures) - - T-Rex, T-Rex Pro, [T-Rex 2](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-T-Rex-2) [**\[!\]**](#special-pairing-procedures) - - [T-Rex Ultra (experimental)](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-T-Rex-Ultra) [**\[!\]**](#special-pairing-procedures) - - [Cheetah (Round/Square) (experimental)](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Cheetah) [**\[!\]**](#special-pairing-procedures) - - [Cheetah Pro](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Cheetah-Pro) [**\[!\]**](#special-pairing-procedures) - - Verge Lite [**\[!\]**](#special-pairing-procedures) - - [X ](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-5) [**\[!\]**](#special-pairing-procedures) -- [Bangle.js](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Bangle.js) -- BFH-16 -- [Casio](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Casio) - - Casio GB-X6900B - - Casio GB-6900B - - Casio GB-5600B - - Casio GW-B5600 - - Casio GMW-B5000 (untested) - - Casio STB-1000 - - Casio GBX-100 - - Casio GBD-100 - - Casio GBD-200 - - Casio GBD-H1000 -- [Femometer Vinca II](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Femometer-Vinca-II) -- [FitPro](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/FitPro) -- Fossil - - [Hybrid HR](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Fossil-Hybrid-HR) [**\[!\]**](#special-pairing-procedures) - - Q Hybrid -- [Galaxy Buds](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Galaxy-Buds) - - [Galaxy Buds](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Galaxy-Buds#user-content-galaxy-buds) - - [Galaxy Buds Live](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Galaxy-Buds#user-content-galaxy-buds-live) - - [Galaxy Buds Pro](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Galaxy-Buds#galaxy-buds-pro) - - [Galaxy Buds2](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Galaxy-Buds#galaxy-buds2) - - [Galaxy Buds2 Pro](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Galaxy-Buds#galaxy-buds2-pro) -- Garmin - - Vívomove HR -- [HPlus Devices (e.g. ZeBand)](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/HPlus) -- ID115 -- [iTag](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/iTag) -- JYou Y5 -- Lefun - - Lefun ID115 Plus - - Other clones: bohemic smart bracelet -- Lenovo - - Watch 9 - - [Watch X (Plus)](https://codeberg.org/mamutcho/Gadgetbridge/wiki) -- Liveview -- Makibes HR3 -- Mi - - [Band, Band 1A, Band 1S](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band), [Band 2](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-2), [Band 3](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-3) - - [Band 4](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-4), [Band 5](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-5), [Band 6](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-6) [**\[!\]**](#special-pairing-procedures) - - [Xiaomi Smart Band 7](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-7) [**\[!\]**](#special-pairing-procedures) - - Xiaomi Temperature and Humidity Monitor Clock (LYWSD02/LYWSD02MMC) (partial support) - - Scale 2 (Currently only displays a toast after stepping on the scale) -- [MyKronoz ZeTime](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/MyKronoz-ZeTime) -- NO.1 F1 -- [Nothing Ear (1)](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Nothing-Ear-%281%29) -- [Nut Mini 3, Nut 2 and possibly others](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Nut) -- Pebble - - [Pebble, Steel, Time, Time Steel, Time Round, 2](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Pebble) -- PineTime (InfiniTime Firmware) -- Roidmi, Roidmi 3, Mojietu 3 (Bluetooth FM Transmitters) -- [SMA](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/SMA) Q2 (SMA-Q2-OSS Firmware) -- Sony - - [Headphones](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Sony-Headphones) - - LinkBuds S - - WH-1000XM2, WH-1000XM3, WH-1000XM4, WH-1000XM5 - - WF-SP800N - - WF-1000XM3, WF-1000XM4, WF-1000XM5 (experimental) - - [Wena 3](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Sony-Wena-3) -- Teclast H10, H30 -- TLW64 -- Vibratissimo (Experimental) -- Wasp-OS devices -- XWatch (Affordable Chinese Casio-like smartwatches) -- [Zepp E](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-GTR) [**\[!\]**](#special-pairing-procedures) -- [Shell Racing cars](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/SuperCars) (BLE RC car models) -- [Flipper Zero](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Flipper-Zero) (Multi-tool Device for Geeks) -- VESC (BLDC controller VESC) -- UM25 (USB Voltage meter) - - - -### Special Pairing Procedures - -Some Huami / Amazfit / Mi / Zepp devices can only be paired with Gadgetbridge using a secret key that has to be obtained once using the proprietary app with an account. Detailed instructions in the wiki: [Huami Server Pairing](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Huami-Server-Pairing) - -The Fossil Hybrid HR also requires using the proprietary app, but with a more complicated procedure. Details in the wiki: [Hybrid HR](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Fossil-Hybrid-HR). +Please see the [Gadgets](https://gadgetbridge.org/gadgets/) page on the website for a complete list of supported devices. ## Features -Please see [FEATURES.md](https://codeberg.org/Freeyourgadget/Gadgetbridge/src/master/FEATURES.md) +Please see the [Features](https://gadgetbridge.org/basics/features/) page on the website. ## Authors ### Core Team (in order of first code contribution) diff --git a/app/build.gradle b/app/build.gradle index 2e33435bb..ed5180e8b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -78,17 +78,19 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } - compileSdkVersion 33 - buildToolsVersion "33.0.0" + + namespace 'nodomain.freeyourgadget.gadgetbridge' defaultConfig { applicationId "nodomain.freeyourgadget.gadgetbridge" + minSdkVersion 21 targetSdkVersion 33 + compileSdk 33 // Note: always bump BOTH versionCode and versionName! - versionName "0.76.1" - versionCode 226 + versionName "0.79.0" + versionCode 229 vectorDrawables.useSupportLibrary = true buildConfigField "String", "GIT_HASH_SHORT", "\"${getGitHashShort()}\"" buildConfigField "boolean", "INTERNET_ACCESS", "false" @@ -112,8 +114,8 @@ android { flavorDimensions "device_type" productFlavors { - main { - // Ensure that when starting from scratch, 'main' is selected, not 'banglejs' + mainline { + // Ensure that when starting from scratch, 'mainline' is selected, not 'banglejs' getIsDefault().set(true) // the default build product flavor dimension "device_type" @@ -162,20 +164,20 @@ android { resValue "string", "about_activity_title", "@string/about_activity_title_main_nightly" resValue "string", "about_description", "@string/about_description_main_nightly" resValue "string", "gadgetbridge_running", "@string/gadgetbridge_running_main_nightly" - + debuggable true if (System.getProperty("nightly_store_file") != null) { signingConfig signingConfigs.nightly //this was conflicting with regular debug type (it should not), so it is only available for CI builds: - productFlavors.main.resValue "string", "pebble_content_provider", "com.getpebble.android.provider" - productFlavors.main.resValue "string", "app_name", "@string/application_name_main_nightly" - productFlavors.main.resValue "string", "title_activity_controlcenter", "@string/title_activity_controlcenter_main_nightly" - productFlavors.main.resValue "string", "about_activity_title", "@string/about_activity_title_main_nightly" - productFlavors.main.resValue "string", "about_description", "@string/about_description_main_nightly" - productFlavors.main.resValue "string", "gadgetbridge_running", "@string/gadgetbridge_running_main_nightly" - + productFlavors.mainline.resValue "string", "pebble_content_provider", "com.getpebble.android.provider" + productFlavors.mainline.resValue "string", "app_name", "@string/application_name_main_nightly" + productFlavors.mainline.resValue "string", "title_activity_controlcenter", "@string/title_activity_controlcenter_main_nightly" + productFlavors.mainline.resValue "string", "about_activity_title", "@string/about_activity_title_main_nightly" + productFlavors.mainline.resValue "string", "about_description", "@string/about_description_main_nightly" + productFlavors.mainline.resValue "string", "gadgetbridge_running", "@string/gadgetbridge_running_main_nightly" + //keep the pebble provider for people who want this. In case of need we can create Banglejs Nopebble productFlavors.banglejs.resValue "string", "pebble_content_provider", "com.getpebble.android.provider" productFlavors.banglejs.resValue "string", "app_name", "@string/application_name_banglejs_nightly" @@ -222,10 +224,9 @@ android { } - - lintOptions { + lint { abortOnError ABORT_ON_CHECK_FAILURE - lintConfig file("${project.rootDir}/app/src/main/lint.xml") + lintConfig file("$rootDir/app/src/main/lint.xml") // If true, generate an HTML report (with issue explanations, sourcecode, etc) htmlReport true // Optional path to report (default will be lint-results.html in the builddir) @@ -251,7 +252,7 @@ dependencies { // testImplementation "ch.qos.logback:logback-core:1.1.3" implementation 'com.android.support.constraint:constraint-layout:2.0.4' testImplementation "junit:junit:4.13.2" - testImplementation "org.mockito:mockito-core:1.10.19" + testImplementation "org.mockito:mockito-core:2.28.2" testImplementation "org.robolectric:robolectric:4.8.2" testImplementation "org.hamcrest:hamcrest-library:1.3" testImplementation "com.google.code.gson:gson:2.8.9" @@ -289,9 +290,15 @@ dependencies { implementation 'com.jaredrummler:colorpicker:1.0.2' // implementation project(":DaoCore") implementation 'com.github.wax911:android-emojify:0.1.7' - implementation 'com.google.protobuf:protobuf-javalite:3.10.0' + implementation 'com.google.protobuf:protobuf-javalite:3.21.7' implementation 'com.android.volley:volley:1.2.1' + // Bouncy Castle is included directly in GB, to avoid pulling the entire dependency + // It's included in the org.bouncycastle.shaded package, to fix conflicts with + // roboelectric + //implementation 'org.bouncycastle:bcpkix-jdk18on:1.76' + //implementation 'org.bouncycastle:bcprov-jdk18on:1.76' + // NON-FOSS dependencies // implementation('androidx.core:core-google-shortcuts:1.0.1') { // exclude group:'com.google.android.gms' @@ -399,7 +406,7 @@ tasks.clean.dependsOn(tasks.cleanGenerated) protobuf { protoc { - artifact = 'com.google.protobuf:protoc:3.10.0' + artifact = 'com.google.protobuf:protoc:3.21.7' } generateProtoTasks { all().each { task -> diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9842ee18a..33a4d3585 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,6 @@ + xmlns:tools="http://schemas.android.com/tools"> Einstellungen Allgemeine Einstellungen - Verbinde, wenn Bluetooth eingeschaltet ist + Verbindung mit Gadgetbridge-Gerät(en) herstellen, wenn Bluetooth eingeschaltet ist Automatisch starten Verbindung automatisch wiederherstellen Bevorzugter Audioplayer @@ -1030,7 +1030,7 @@ Beschriftung bearbeiten Swolf-Index BasishÃļhe - Swolf-Index + SWOLF Individuell ausgewählte Elemente Zu Filter hinzufÃŧgen Alle Geräte @@ -1487,7 +1487,7 @@ Sony WF-SP800N Ausschalten Ausschalten - Sind Sie sicher, dass Sie das Gerät abschalten wollen\? + Bist du sicher, dass du das Gerät abschalten willst? Nicht unterstÃŧtzte Geräte finden Min Schrittlänge Wenn du diese Option aktivierst, werden beim Scannen alle erkannten Bluetooth-Geräte angezeigt. Durch kurzes Antippen werden Gerätename und MAC-Adresse in die Zwischenablage kopiert. Langes DrÃŧcken startet den Dialog \"Testgerät hinzufÃŧgen\". Kann zu Problemen mit einfrierender App fÃŧhren. @@ -1598,13 +1598,13 @@ \n \nAufgrund der Richtlinien des Google Play Stores ist es uns nicht erlaubt, einen Spendenlink in der App selbst einzubauen, aber wenn dir diese App gefällt, ziehe bitte eine Spende Ãŧber die Gadgetbridge-Homepage unten in Erwägung. Gadgetbridge (Nightly) - Gadgetbridge Nightly - Über Gadgetbridge Nightly + Gadgetbridge (Nightly) + Über Gadgetbridge (Nightly) Cloudloser und freier quelloffener Ersatz fÃŧr Closed-Source Android-Gadget-Apps von Anbietern. Nächtliche VerÃļffentlichungen von Gadgetbridge. Es kann nicht installiert werden, wenn du bereits die Gadgetbridge- oder die Pebble-App installiert hast, da es einen Konflikt mit dem Pebble-Anbieter gibt. Nightly GB läuft Gadgetbridge (Nightly, kein Pebble-Anbieter) - Gadgetbridge Nightly kein Pebble - Über Gadgetbridge Nightly kein Pebble + Gadgetbridge (Nightly, kein Pebble) + Über Gadgetbridge (Nightly, kein Pebble) Nightly kein Pebble GB läuft Empfindlichkeit Normal @@ -1824,7 +1824,7 @@ Android-Begleit-App fÃŧr Bangle.js, die auf dem Gadgetbridge-Projekt aufbaut, mit zusätzlichem Internetzugang. \n \nAufgrund der Richtlinien des Google Play Stores ist es uns nicht erlaubt, einen Spendenlink in der App selbst einzubauen, aber wenn dir diese App gefällt, ziehe bitte eine Spende Ãŧber die Gadgetbridge-Homepage unten in Erwägung. - Nightly Bangle.js wird ausgefÃŧhrt + Bangle.js (Nightly) wird ausgefÃŧhrt StÃŧndlicher Klang Die Uhr piept einmal pro Stunde Meine täglichen Schritterfolge! @@ -1889,7 +1889,7 @@ 40 BPM 45 BPM Erinnerung zur Entspannung - SPO2-Alarmschwelle + SpO2-Alarmschwelle 7 Sekunden 8 Sekunden 9 Sekunden @@ -1966,7 +1966,7 @@ Sony WH-1000XM2 Sony WF-1000XM4 Ausgewogen - Dual Band + Doppelband Stromsparendes GPS GPS GPS + BDS @@ -2010,7 +2010,7 @@ Offline Stimme Bildschirm leuchtet immer Ton & Vibration - Single Band + Einzelband GPS + GALILEO Text in Sprache Reagiert beim Drehen des Handgelenks @@ -2464,7 +2464,7 @@ UngÃŧltiges JSON fÃŧr MenÃŧstruktur GrÃļßere SchriftgrÃļße MenÃŧstruktur zurÃŧcksetzen - SchriftgrÃļße in Kalender, Benachrichtigungen, etc. vergrÃļßern + SchriftgrÃļße in Kalender, Benachrichtigungen usw. vergrÃļßern Anrufbenachrichtigungen empfangen Eingehende Anrufvibration LED-Farbe bei eingehendem Anruf @@ -2479,7 +2479,7 @@ Normaler Modus (60s-90s) Zepp Pay Schneller Modus (30s) - Arbeitsprofilbenachrichtigungen ignorieren + Ignoriere Arbeitsprofilbenachrichtigungen Amazfit Balance yd ZÃŧge/min @@ -2492,6 +2492,322 @@ Apps-VerknÃŧpfungen Benachrichtigungen von Apps im Arbeitsprofil nicht an die Uhr senden Thermometer - Gebräuchliche Symbole + Häufige Symbole Femometer Vinca II + Wanderung + Wrestling + Redmi Smart Band Pro + Bereitschaft + Xiaomi Watch Lite + Amazfit Active Edge + Klingeln (Vibration/Ton) bei eingehenden Anrufen + Alarm fÃŧr Mail-Benachrichtigungen + Xiaomi Smart Band 7 Pro + Klatsche in die Hände, um den Bildschirm zu aktivieren" + ColaCao 2021 + ColaCao 2023 + Redmi Watch 3 Active + Redmi Smart Band 2 + Steh-Zeit + Aktiv-Zeit + Sende Benachrichtigungen + Sende App-Benachrichtigungen an das Gerät + Nachrichtenvorschau in Titelleiste anzeigen + Zeigt vom Gerät zugelassene Vorschau der Nachricht im Benachrichtigungstitel an + Alarmton fÃŧr Kalender-Erinnerungen + Alarm (Vibration/Ton) fÃŧr Kalender-Erinnerungen + Klingelton bei eingehenden Anrufen + Alarm (Vibration/Ton) fÃŧr Mail-Benachrichtigungen + Alarm fÃŧr SMS-Benachrichtigungen + Alarm fÃŧr andere Benachrichtigungen + Alarm (Vibration/Ton) fÃŧr Benachrichtigungen in anderen Kategorien + Xiaomi Smart Band 8 + Mijia Temperatur- und Feuchtigkeitssensor 2 + KÃļrperliche Zusammensetzung + Amazfit Active + Zusätzliches Ziel + Redmi Watch 2 Lite + Aufwachen + Der Bildschirm schaltet sich aus, wenn das Mikrofon eine Zeit lang Stille erkannt hat + Informationen zur Aktivität + Das Passwort muss 4-stellig sein und darf nur Zahlen enthalten + Geländelauf + Verbindungstyp erzwingen + Installation des Zifferblatts abgeschlossen + Installation des Zifferblatts fehlgeschlagen + Automatische Pulsmessung aktivieren + Weitläufiger Himmel + Reines Weiß + FÃŧgt abgerundete Rechtecke um Startbildschirmsymbole hinzu + \'HR Menu Companion\" ist wahrscheinlich nicht installiert + 7-Tage-Fortschritt + Täglicher Fortschritt + Vitalitätswert + ReisefÃŧhrer + Telefonstummschaltung + Normal / Vibration + Intelligente Vibration + Bettzeit + Automatisches Ausschalten + Benachrichtigung auf dem Gerät, wenn die Verbindung zu Bluetooth getrennt wird. + Ganztägig + Optionen erzwingen + Intelligenten Alarm erzwingen + Status der Verbindung + UnterstÃŧtzung von \"Bitte-Nicht-StÃļren\" erzwingen + Durchschnittlicher Blutsauerstoff + Telefon suchen deaktivieren, wenn Bitte-Nicht-StÃļren aktiv ist + Ablehnen von Anrufen aktivieren + Automatische SpO2-Messung aktivieren + Datensynchronisierung im Hintergrund + Alarm Einstellungen + Der Akku des Geräts ist zu schwach + Normal / Lautlos + Vibration / Lautlos + Erneutes Klatschen schaltet den Bildschirm aus" + Statistiken abrufen + Temperaturdaten abrufen + Tragemodus + Sternenhimmel + Erlaube Wena, Gadgetbridge in regelmäßigen Abständen aufzufordern, Aktivitätsdaten vom Gerät herunterzuladen + Navigations-App nicht auf der Uhr installiert + Ablehnen + Zeitplan fÃŧr Schlafmodus + Sendet eine Erinnerung und schaltet zur Bettzeit in den Schlafmodus. Zur geplanten Aufwachzeit ertÃļnt der Weckalarm. + Erhalte eine Benachrichtigung, wenn dein Vitalitätswert in den letzten 7 Tagen 30, 60 oder 100 erreicht hat + Erhalte eine Benachrichtigung, wenn du die maximale Anzahl an Vitalitätspunkten fÃŧr den Tag erreicht hast + Armband + Halskette (Halsband) + Kieselsteine (Schuhschnalle) + Dänisch + Alarm (Vibration/Ton) fÃŧr SMS (Textnachrichten) + Lange drÃŧcken + Summer-Intensität + Blitzschlag + Zifferblatt hochladenâ€Ļ + Zifferblatt hochladen + Vibrieren bei neuer Anweisung + In den Vordergrund rÃŧcken + Gerätebezeichnung + Nicht festgelegt + Bist du sicher, dass du \'%1$s\' lÃļschen mÃļchtest? + Nach unten verschieben + Protokoll-Version + Flache Strecke + Annehmen von Anrufen aktivieren + Navigation gestartet, aber die Navigation-App ist nicht auf der Uhr installiert. Bitte installiere sie Ãŧber den App-Manager. + Ob die Navigations-App automatisch in den Vordergrund kommen soll, wenn sie eine Navigationsanweisung erhält + Du kannst versuchen, den Verbindungstyp zu erzwingen, falls dein Gerät nicht auf Gadgetbridge reagiert + Automatisch + Bluetooth-LE + Bitte-Nicht-StÃļren - nur Alarme + Navigations-Apps + Dient zur Auswahl der Version von OsmAnd, mit der eine Verbindung hergestellt werden soll + Einstellungen zur Navigation + OsmAnd Paketname + Bitte alle Widgets auswählen + 1 oben, 2 unten + Unbekanntes Workout - %s + Ausschaltzeit + Bitte-Nicht-StÃļren - Aus + Freie Kombination + Debug-Anfrage + Aktivitätsbildschirm + Verbindung nur mit verbundenen Geräten wiederherstellen + Verbindung nur mit verbundenen Geräten wiederherstellen, statt mit allen Geräten neu zu verbinden + Transparenz + Arbeitsmodus + Eine Debug-Anfrage an das Huawei-Gerät senden + Symbol fÃŧr aktuelle Wetterbedingungen in der oberen linken Ecke des Startbildschirms anzeigen + App-Name in der Benachrichtigung + Bitte-Nicht-StÃļren - Ein + UnterstÃŧtzung von \"Bitte-Nicht-StÃļren\" erzwingen +\nVERWENDUNG AUF EIGENE GEFAHR + App-spezifische Einstellungen fÃŧr Benachrichtigungen + Seriennummer + Statistiken + Meldungen + 2 oben, 1 unten + 2 oben, 2 unten + Widget-Anordnung + 1 Widget + Nach oben verschieben + 2 Widgets + Ignorieren (Stille) + Verbesserte SchlafÃŧberwachung + UnterstÃŧtzung fÃŧr intelligente Alarme erzwingen +\nVERWENDUNG AUF EIGENE GEFAHR + Starte Aktivitätstimer + Anweisungen fÃŧr die Navigation + Ob die Uhr bei jeder neuen oder geänderten Navigationsanweisung vibrieren soll (nur wenn die App im Vordergrund ist) + Konfiguriere das Verhalten der Navigations-App + Manuell + Bitte-Nicht-StÃļren wenn nicht getragen + Widget + Version 2 + Version 3 + Google Maps + Nothing Ear (2) + Leichte aktive GeräuschunterdrÃŧckung + HUAWEI TruSleep â„ĸ + Bitte-Nicht-StÃļren - nur Prioritäten + Honor Band 4 + Honor Band 5 + Honor Band 6 + Honor Band 7 + Huawei Band (AW70) + Huawei Band 6 + Huawei Band 7 + Huawei Band 8 + Huawei Watch GT + Huawei Band 4 (Pro) + Huawei Watch GT 2 (Pro) + Huawei Watch GT 2e + Huawei Talk Band B6 + Huawei Watch GT 3 (Pro) + Laufen + Xiaomi Watch S1 Active + Mi Watch Color Sport + Pixoo + Version 1 + Xiaomi Watch S1 + Xiaomi Watch S3 + Xiaomi Watch S1 Pro + Xiaomi Smart Band 8 Pro + Mijia MHO-C303 + Honor Band 3 + Nothing Ear (Stick) + JSON-MenÃŧstruktur in Gadgetbridge gesetzt + The Ultima + City of Speed + Widget-Bildschirm + Es mÃŧssen mindestens %1$s Widget-Bildschirme vorhanden sein + Das Gerät hat keine freien Plätze fÃŧr Widget-Bildschirme (insgesamt: %1$s) + Überwache deine Schlafqualität und dein Atemmuster in Echtzeit. +\nAnalysiere dein Schlafmuster und diagnostiziere genau 6 Arten von Schlafproblemen. + Doppelt DrÃŧcken + Der Tag beginnt um + Schritt hoch + Schritt runter + Kann bei der korrekten Schlaferkennung helfen. Sofort sichtbar in der Ansicht der täglichen Aktivitäten. + Deaktiviere nicht das Kontrollkästchen fÃŧr das intelligente Aufwecken. + Einige Geräte behaupten fälschlicherweise, dass sie einige Optionen nicht unterstÃŧtzen. Diese Einstellungen kÃļnnen verwendet werden, um sie trotzdem zu aktivieren. +\nVERWENDUNG AUF EIGENE GEFAHR +\nLese auf der Webseite + Aktiviere das Abweisen von Anrufen vom Gerät + Aktiviere die Annahme von Anrufen vom Gerät + Reparieren der Trainingsdaten + Aktiviere nicht das Kontrollkästchen fÃŧr das intelligente Aufwecken. + Kann bei der korrekten Schlaferkennung helfen. Sofort sichtbar in der Ansicht der täglichen Aktivitäten. + Widget-Bildschirm lÃļschen + Erzwinge Standort der Uhr + Dies wird nur nach bestimmten Aktualisierungen geschehen + Schaltflächenaktion + Welche Aktion wird ausgefÃŧhrt wenn ein eingehender Anruf von der Uhr abgelehnt wird + Nur falls beim \"Hochheben Display aktivieren\" aktiviert ist + Fokus + Methode zur Ablehnung von Anrufen + Zeit fortsetzen + 155 Schläge/min + 165 Schläge/min + 175 Schläge/min + 185 Schläge/min + 195 Schläge/min + 205 Schläge/min + Sony WI-SP600N + OsmAnd(+) + Bluetooth-Classic + Alarmschwelle fÃŧr hohe Aktivität der Herzfrequenz + Deaktivieren von Hydrationswarnungen fÃŧr ein bestimmtes Zeitintervall + Karate + Kendo + Weitsprung + Hochsprung + Eishockey + Honor MagicWatch 2 + Trampolin + Speerwurf + CMF Watch Pro + Bauchtanz + Jagen + Erzwinge UnterstÃŧtzung fÃŧr Standort der Uhr +\nBENUTZUNG AUF EIGENE GEFAHR + Outdoor-Wandern + Bergsteigen + Skateboarden + Wintersport + Skilanglauf + Curling + Jetski fahren + Segeln + Bergwandern + Indoor-Laufen + Crosstrainer + Ruderer + FitnessÃŧbungen + Funktionales Training + KÃļrperliches Training + Taekwondo + Fechten + AbkÃŧhlung + VÃļlkerball + Softball + Federball + Angeln + Treppen + Jazz-Tanz + Lateinischer Tanz + Ballett + Rollschuhlaufen + Martial Arts + Tai-Chi + Hula Hoop + Darten + Bogenschießen + Pferdereiten + Schaukeln + Drachensteigen + Lacross + Freies Training + Leichtathletik + Aerobic-Übung + LiegestÃŧtze + Eislaufen + KÃļrper und Geist + Handrad fahren + Andere Tänze + Querfeldeinlauf + KlimmzÃŧge + Billard + Redmi Watch 2 + Ziehgriff + Rich-Design verwenden + Präfix des Benachrichtigungstitels mit dem Namen der Quellanwendung + Endstatus des Weckens ignorieren + Startstatus beim Aufwachen ignorieren + Intelligente Alarmspanne + Widget-Untertyp + Widget-Bildschirm %s + Begleit-App MenÃŧ Ãļffnen + Stepper + Sit-ups + Fitness-Spiele + Beweglichkeit + Kurzhantel + CrossFit + Horizontale Stange + Barren + Cross-Training + Plank + Rollen + Seiltraining + Smith-Gerät + Dynamischer Zyklus + Pickleball + Kartsport + Diskus-Sport + Kabaddi + Australisches Football + Shot + Statusseite bereitstellen \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 94fb52682..8661d7963 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -52,7 +52,7 @@ Ajustes Ajustes generales - Conectarse al dispositivo cuando el Bluetooth estÊ activado + Conectarse al (los) dispositivo(s) Gadgetbridge cuando el Bluetooth estÃĄ(n) activado(s) Iniciar automÃĄticamente Reconectar automÃĄticamente Reproductor de audio favorito @@ -1036,13 +1036,13 @@ Pulsaciones km lat/min - km/min + min/km m/s km/s kcal vueltas estilo de nataciÃŗn - índice swolf + SWOLF índice swolf seg p @@ -1053,7 +1053,7 @@ cm m Vueltas - estilo de nataciÃŗn + Estilo de nataciÃŗn Ritmo mÃĄs rÃĄpido Ritmo mÃĄs lento MÃĄximo @@ -1959,7 +1959,7 @@ Entrenamiento bÃĄsico Categorías de entrenamiento Categorías de entrenamientos para detectar automÃĄticamente - Alertas + Alerta Notificar cuando se detecta un entrenamiento Volumen sonido ambiente izquierdo ReducciÃŗn del ruido @@ -2273,8 +2273,8 @@ PAI al mes +%d %d min - PAI total - Aumento del PAI por día + Total + Aumento del día Instalar Catima Grupos a sincronizar Sincronizar sÃŗlo tarjetas favoritas @@ -2519,4 +2519,206 @@ Femometer Vinca II Obtener las estadísticas Obtener los datos de la temperatura + Ignorar (silencio) + Tasa media de carrera + Velocidad en ciudad + Rechazar + MÊtodo de rechazo de llamadas + Se ha iniciado la navegaciÃŗn pero la aplicaciÃŗn de navegaciÃŗn no estÃĄ instalada en el reloj. Por favor, instÃĄlela desde App Manager. + yarda + serie/min + QuÊ acciÃŗn se realiza cuando se rechaza una llamada entrante desde el reloj + Frecuencia mÃĄxima de carrera + Longitud de la pista + Total de carreras + La aplicaciÃŗn de navegaciÃŗn no estÃĄ instalada en el reloj + Amazfit Active Edge + Promedio de oxígeno en sangre + Amazfit Active + Anteponer al título de la notificaciÃŗn el nombre de la aplicaciÃŗn de origen + Permite seleccionar la versiÃŗn de OsmAnd a la que conectarse + Nombre de la aplicaciÃŗn en la notificaciÃŗn + Nombre del paquete OsmAnd + Aplicaciones de navegaciÃŗn + Preferencias de navegaciÃŗn + Google Maps + OsmAnd(+) + Alerta para otras notificaciones + Alerta de las notificaciones en el calendario + Alerta (vibraciÃŗn/pitido) de llamadas entrantes + Xiaomi Smart Band 8 + Ejecutando + Horario del modo de reposo + Alerta de llamadas entrantes + Hora de acostarse + Alerta de notificaciones por correo electrÃŗnico + Alerta (vibraciÃŗn/pitido) para notificaciones por correo electrÃŗnico + Enfocar + Alerta (vibraciÃŗn/pitido) para notificaciones SMS (mensajes de texto) + Mostrar una vista previa del mensaje en el título + Redmi Watch 3 Active + Alerta (vibraciÃŗn/pitido) para las notificaciones de la agenda + NÃēmero de serie + DanÊs + Despertar + Estadísticas + Alerta (vibraciÃŗn/pitido) para las notificaciones de la otra categoría + Xiaomi Watch Lite + Muestra una vista previa del mensaje en el título de una notificaciÃŗn segÃēn lo permita el dispositivo + Envía un recordatorio y entra en modo reposo a la hora de acostarte. A la hora programada para despertarse, sonarÃĄ la alarma del despertador. + Alerta de notificaciones por SMS + Alertas + Xiaomi Watch S1 Active + Xiaomi Smart Band 7 Pro + Mi Watch Color Sport + Pixoo + Enviar notificaciones de las aplicaciones al dispositivo + Enviar notificaciones + No molestar - Activado + No molestar - Solo alarmas + No molestar - SÃŗlo con prioridad + No fijado + No molestar - Desactivado + Objetivo secundario + tiempo de reposo + Tiempo activo + Avances diarios + Progreso en 7 días + Recibir una notificaciÃŗn cuando su puntuaciÃŗn de vitalidad alcance 30, 60 o 100 en los Ãēltimos 7 días + Nivel de vitalidad + Recibir una notificaciÃŗn cuando alcances el nÃēmero mÃĄximo de puntos de vitalidad del día + Modo silencio del telÊfono + Modo de uso + Pebble (hebilla de zapato) + Normal / VibraciÃŗn + Vibrar / Silencio + Redmi Smart Band 2 + Pulsera (Pulsera) + Collar (tira para el cuello) + Normal / Silencio + Carrera de montaÃąa + DiseÃąo del widget + El dispositivo no tiene espacio libre para pantallas con widgets (espacio total: %1$s) + Entrenamiento desconocido - %s + Subir + ÂŋSeguro que quieres borrar \'%1$s\'? + Senderismo + 2 widgets + Eliminar la pantalla de widgets + 1 superior, 2 inferiores + 2 superiores, 2 inferiores + Debe haber un mínimo de %1$s pantallas + Widget + Pantalla con los widgets + Bajar + 1 widget + Subtipo de widget + Pantalla %s + Por favor, seleccione todos los widgets + 2 superior, 1 inferior + Lucha libre + ColaCao 2023 + ColaCao 2021 + Aplaudir para subir la pantalla" + Si vuelves a aplaudir, se apagarÃĄ la pantalla" + Sensor de temperatura y humedad Mijia 2 + La pantalla se apagarÃĄ despuÊs de que el micrÃŗfono haya detectado un silencio durante un tiempo + Instrucciones de navegaciÃŗn + Configurar el comportamiento de la aplicaciÃŗn de navegaciÃŗn en el reloj + Si la aplicaciÃŗn de navegaciÃŗn debe pasar automÃĄticamente a primer plano cuando recibe una instrucciÃŗn de navegaciÃŗn + Vibrar al recibir una nueva instrucciÃŗn + Pasar a primer plano + Nombre del dispositivo + Si el reloj debe vibrar con cada instrucciÃŗn de navegaciÃŗn nueva o modificada (sÃŗlo cuando la aplicaciÃŗn estÃĄ en primer plano) + Redmi Watch 2 Lite + Estado de la conexiÃŗn + Redmi Smart Band Pro + Nothing Ear (2) + Nothing Ear (Stick) + Transparencia + CancelaciÃŗn activa del ruido ligera + No molestar cuando no lo use + Manual + Forzar la funciÃŗn No molestar + Forzar el soporte de No Molestar. +\nUTILÍCELO BAJO SU PROPIA RESPONSABILIDAD + Ignorar el estado de inicio del despertador + Puede ayudar a detectar correctamente el sueÃąo. Visible inmediatamente en la vista de actividades diarias. + Ignorar estado de fin de despertador + NotificaciÃŗn en el dispositivo cuando se desconecta de BT. + Todo el día + Honor Band 3 + Honor Band 4 + Honor Band 5 + Honor Band 6 + Honor Band 7 + Huawei Band 6 + Huawei Band 7 + Huawei Band 8 + Huawei Watch GT + Huawei Band 4 (Pro) + Huawei Watch GT 2 (Pro) + Huawei Watch GT 2e + Huawei Talk Band B6 + Huawei Watch GT 3 (Pro) + Modo de trabajo + No desmarque la casilla de activaciÃŗn inteligente. + HUAWEI TruSleep â„ĸ + Monitorice la calidad de su sueÃąo y su patrÃŗn respiratorio en tiempo real. +\nAnaliza tus patrones de sueÃąo y diagnostica con precisiÃŗn 6 tipos de problemas de sueÃąo. + Control del sueÃąo mejorado + Permitir aceptar llamadas + Activar la aceptaciÃŗn de llamadas desde el dispositivo + Permitir rechazar llamadas + Activar el rechazo de llamadas desde el dispositivo + Desactivar encontrar mi telÊfono cuando no molestar estÃĄ activo + Activar la mediciÃŗn automÃĄtica del ritmo cardíaco + Activar la mediciÃŗn automÃĄtica de SpO2 + OpciÃŗn obligatoria + Forzar la alarma inteligente + Lugar de desgaste de la fuerza + Soporte de localizaciÃŗn de desgaste forzoso. +\nUSO BAJO SU PROPIA RESPONSABILIDAD + Reparar datos de entrenamiento + Esto sÃŗlo harÃĄ algo despuÊs de ciertas actualizaciones + Enviar una solicitud de depuraciÃŗn al dispositivo Huawei + Solicitud de depuraciÃŗn + Reconectar sÃŗlo a los dispositivos conectados + Reconectarse sÃŗlo a los dispositivos conectados, en lugar de volver a conectarse a todos los dispositivos + SÃŗlo si se activa la visualizaciÃŗn al subir + Huawei Band (AW70) + No marque la casilla de activaciÃŗn inteligente. + Algunos dispositivos afirman falsamente no ser compatibles con algunas opciones. Esta configuraciÃŗn se puede utilizar para habilitarlos de todos modos. +\nUTILÍZALO BAJO TU PROPIA RESPONSABILIDAD +\nLea la wiki + Forzar el soporte de alarmas inteligentes. +\nUSO BAJO SU PROPIA RESPONSABILIDAD + Puede ayudar a detectar correctamente el sueÃąo. Visible inmediatamente en la vista de actividades diarias. + La batería del dispositivo estÃĄ demasiado baja + Intensidad del zumbador + La contraseÃąa debe tener 4 dígitos, utilizando sÃŗlo nÃēmeros + Xiaomi Smart Band 8 Pro + VersiÃŗn 2 + Forzar el tipo de conexiÃŗn + InformaciÃŗn de la actividad + Mijia MHO-C303 + VersiÃŗn 1 + VersiÃŗn 3 + VersiÃŗn del protocolo + Xiaomi Watch S1 + Xiaomi Watch S3 + Xiaomi Watch S1 Pro + Subiendo esfera del relojâ€Ļ + Subiendo esfera del reloj + InstalaciÃŗn de la esfera del reloj completada + La instalaciÃŗn de la esfera del reloj fallÃŗ + AutomÃĄtico + Bluetooth de baja energía + Bluetooth + Puedes intentar forzar el tipo de conexiÃŗn en caso de que tu dispositivo no responda a Gadgetbridge + Ruta llana + Sony WI-SP600N + 155 bpm + 165 bpm + 175 lpm \ No newline at end of file diff --git a/app/src/main/res/values-fr-rCA/strings.xml b/app/src/main/res/values-fr-rCA/strings.xml index fdbef6cda..0cffe1aa9 100644 --- a/app/src/main/res/values-fr-rCA/strings.xml +++ b/app/src/main/res/values-fr-rCA/strings.xml @@ -898,4 +898,6 @@ \nÀ VOS RISQUES ET PÉRILS! DÊfinir un alias Rechercher %1$s\? + Alerte pour les notifications de courriel + Alerte (vibre/bipe) pour les notifications de courriel \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 8810a5221..60d898ee9 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -825,7 +825,7 @@ Temps de sommeil prÊfÊrÊ en heures Permet les alertes de calendrier, mÃĒme en cas de dÊconnexion Sync. calendrier des Êvènements FrÊquence cardiaque - Étapes + Pas Date Les minutes actives Calories @@ -927,7 +927,7 @@ Temps de sommeil prÊfÊrÊ en heures Étiquette Filtre Effacer les filtres - A + À De Statistiques sur les activitÊs sportives Filtre sur les activitÊs sportives @@ -968,7 +968,7 @@ Temps de sommeil prÊfÊrÊ en heures Plat Ascendant Type de nage - index swolf + SWOLF Moyenne de pause FoulÊes moyennes Distance moyenne de foulÊe @@ -990,7 +990,7 @@ Temps de sommeil prÊfÊrÊ en heures Erreur lors de la rÊcupÊration des appareils de la base de donnÊes Erreur pour configurer l\'alias: Erreur à l\'exportation des prÊfÊrences spÊcifiques de cet appareil - Demander et vÊrifier les autorisations manquantes mÃĒme si elles ne sont pas immÊdiatement nÊcessaires. Les dÊsactiver uniquement si votre appareil ne supporte pas ces fonctions. Refuser une autorisation peut provoquer des problèmes ! + Demander et vÊrifier les autorisations manquantes mÃĒme si elles ne sont pas immÊdiatement nÊcessaires. DÊsactiver uniquement si votre appareil ne supporte pas ces fonctions. Refuser une autorisation peut provoquer des problèmes ! VÊrifier l\'Êtat des autorisations Le dÊmarrage du service en arrière-plan n\'a pas rÊussi à cause d\'une exception. Erreur: CLÉ NÉCESSAIRE, APPUI LONG POUR ENTRER @@ -1008,8 +1008,8 @@ Temps de sommeil prÊfÊrÊ en heures Contributeurs Équipe principal (dans l\'ordre de la première contribution au code) Un remplaçant libre et sans cloud aux applications Android propriÊtaires des fabricants de vos bracelets. - A propos de Gadgetbridge - A propos + À propos de Gadgetbridge + À propos Horloge mondiale Stress Respiration @@ -1033,7 +1033,7 @@ Temps de sommeil prÊfÊrÊ en heures EntraÃŽneur elliptique VÊlo d\'intÊrieur Natation (en eau libre) - UtilisÊ par le fournisseur mÊtÊo de LineageOS, les autres versions d\'Android doivent utilisÊes une application comme Weather notification. Se reporter au wiki Gadgetbridge. + UtilisÊ par le fournisseur mÊtÊo de LineageOS, les autres versions d\'Android doivent utiliser une application comme Weather notification. Se reporter au wiki Gadgetbridge. Vous ÃĒtes sur le point d\'installer le micrologiciel %s dans votre Mi Band 5. \n \nVeuillez installer le fichier .fw, puis le fichier .res. Votre montre redÊmarrera après avoir installÊ le fichier .fw. @@ -1585,16 +1585,16 @@ Temps de sommeil prÊfÊrÊ en heures S\'allumer pour une nouvelle notification Email Bangle.js Gadgetbridge - A propos de Bangle.js Gadgetbridge + À propos de Bangle.js Gadgetbridge Bangle.js Gadgetbridge Application complÊmentaire Android pour Bangle.js ÊlaborÊe sur la base du projet Gadgetbridge, avec des fonctionnalitÊs Internet supplÊmentaires. \n \nEn raison des règles du Google Play Store, nous ne pouvons pas mettre de lien de don directement dans l\'application, mais si vous apprÊciez cette application, pensez à faire un don via la page web de Gadgetbridge ci-dessous. Bangle.js pour Gadgetbridge Bangle.js pour Gadgetbridge - A propos de Gatgetbridge Nightly + À propos de Gatgetbridge Nightly A propos de Bangle.js pour Gadgetbridge - A propos de Gadgetbridge Nightly sans Pebble + À propos de Gadgetbridge Nightly sans Pebble Remplaçant autonome et sous licence libre pour remplacer les applications propriÊtaires des fabricants. Version Nightly de Gadgetbridge. Ne peut pas ÃĒtre installÊ si vous avez dÊja Gadgetbridge ou Pebble d\'installÊ, en raison d\'un conflit avec l\'application Pebble. Gadgetbridage (Nightly, pas defournisseur Pebble) Gadgetbridge Nightly Sans Pebble @@ -1669,7 +1669,7 @@ Temps de sommeil prÊfÊrÊ en heures Permettre aux apps sur cet appareil d\'accÊder à Internet Permettre les intentions Application de suivi sportif - DÊmmare/arrÃĒte le suivi sportif sur le tÊlÊphone si une activitÊ GPS est dÊmarrÊ sur le bracelet + DÊmarre/arrÃĒte le suivi sportif sur le tÊlÊphone si une activitÊ GPS est dÊmarrÊe sur le bracelet Envoyer le GPS durant l\'exercice Envoyer les donnÊes GPS en cours durant un exercice GPS Gadgebridge @@ -1814,7 +1814,7 @@ Temps de sommeil prÊfÊrÊ en heures ÉvÊnements personnalisÊs CrÊation du fichier journal ratÊe, l\'Êcriture du fichier journal est indisponible. RedÊmarrer l\'application pour essayer de nouveau l\'Êcriture du fichier journal. Obtenir les mesures de rythme cardiaque - A propos de Bangle.js Gadgetbridge (Version quotidienne) + À propos de Bangle.js Gadgetbridge (Version quotidienne) Bangle.js Gadgetbridge (Version quotidienne) Bangle.js Gadgetbridge (Version quotidienne) Capteur binaire @@ -1826,13 +1826,13 @@ Temps de sommeil prÊfÊrÊ en heures La montre fera un beep toutes les heures Objectifs de pas Mes rÊsultats quotidiens de pas ! - %1$s a besoin d\'une autorisation pour s\'afficher au-dessus des autres apps afin de pouvoir lanter des activitÊs via la commande vocale lorsque %1$s est en arrière-plan. + %1$s a besoin d\'une autorisation pour s\'afficher au-dessus des autres apps afin de pouvoir lancer des activitÊs via la commande vocale lorsque %1$s est en arrière-plan. \n \nCela peut ÃĒtre utilisÊ pour dÊmarrer une application de musique et jouer un morceau, et de nombreuses autres choses. \n \nMerci de choisir \'%2$s\' puis \'%1$s\' et activer \'Permettre l\'affichage au-dessus des autres apps\', puis faire \'Retour\' pour revenir à %1$s. \n -\nPour empÃĒcher %1$s de demander des permissions se rendre dans les \'RÊgalges\' et dÊcocher \'VÊrifier l\'Êtat des permissions\'. +\nPour empÃĒcher %1$s de demander des permissions se rendre dans les \'RÊglages\' et dÊcocher \'VÊrifier l\'Êtat des permissions\'. \n \nAssurez-vous d\'accorder à %1$s les permissions nÊcessaires pour les fonctionnalitÊs attendues. Heure @@ -2043,7 +2043,7 @@ Temps de sommeil prÊfÊrÊ en heures Objectif quotidien : temps d\'Êlimination de calories en minutes Mode CinÊma Activer le mode sommeil automatiquement lors du port du bracelet durant le sommeil - Tronquer lse horaires lors de la rÊcupÊration + Tronquer les horaires lors de la rÊcupÊration Soleil & Lune Voix hors-ligne Écran tout le temps allumÊ @@ -2053,8 +2053,8 @@ Temps de sommeil prÊfÊrÊ en heures TÊlÊphone LuminositÊ Action sur un appui court du bouton infÊrieur - liste des applications dont les notifications mÊdia doivent ÃĒtre ignorÊes - Traiter les notifications mÊdia avant la liste des app. Si la prÊfÊrence n\'est pas cochÊe, les applicatioms mÊdia doivent ÃĒtre autorisÊes dans la liste des applications pour que les contrôles mÊdia fonctionnent sur l\'appareil. + Liste des applications dont les notifications mÊdia doivent ÃĒtre ignorÊes + Traiter les notifications mÊdia avant la liste des app. Si la prÊfÊrence n\'est pas cochÊe, les applications mÊdia doivent ÃĒtre autorisÊes dans la liste des applications pour que les contrôles mÊdia fonctionnent sur l\'appareil. Supprimer les prÊfÊrences de l\'appareil \? Cela supprimera toutes les prÊfÊrences appareil pour tous les appareils connectÊs. Etes-vous sÃģr \? Éjecter l\'eau @@ -2167,7 +2167,7 @@ Temps de sommeil prÊfÊrÊ en heures Coupler pour les appels Bluetooth Afin de recevoir les appels en Bluetooth, vous devez pairer votre tÊlÊphone avec une seconde instance de la montre. 2. Rendez-vous dans les rÊglages Bluetooth de votre tÊlÊphone, et pairez-le avec le nouvel appareil qui apparaitra (nom similaire à votre montre existante, mais avec un suffixe, du style \"Amazfit GTR 4 - AFC8\". - Masquer lorsque hors de portÊe + Garder en cache lorsque hors de portÊe Envoyer les notifications manquÊes lorsqu\'un appareil se reconnecte après avoir ÊtÊ hors de portÊe Annulation de bruit ←→ Ambiance ←→ Off Appui simple @@ -2499,4 +2499,94 @@ Temps de sommeil prÊfÊrÊ en heures Symboles communs Ignorer les notifications en profil travail Ne pas envoyer les notifications des apps dans le profil travail à la montre + Blanc pur + Ignorer (silence) + Rythme moyen de course + L\'ultima + Vitesse urbaine + Écouteur + Mode normal (60s - 90s) + Rejet + Combinaison libre + Composition corporelle + Zepp Pay + MÊthode de rejet d\'appel + RÊcupÊration des statistiques + Mode Rapide (30s) + Ciel ÊtoilÊ + Balance Amazfit + La navigation a dÊmarrÊe mais pas d\'app de navigation installÊe dans la montre. Merci d\'en installer une depuis le Gestionnaire d\'App. + yard + str/min + MÊthode de mesure + Quelle action à faire quand un appel entrant est rejetÊ depuis la montre + Amazfit Active Edge + RÊcupÊration des donnÊes de tempÊrature + Mode PrÊcis (3 min) + Ciel clair + Raccourcis Exercice + Vitesse maximale de course + Flash lumineux + Longueur de ligne + Moyenne de l\'oxygène dans le sang + Vitesse totale + Raccourcis Apps + Guide + PrÊparation + Amazfit Active + Thermomètre + Femometer Vinca II + App de navigation non installÊe sur le tÊlÊphone + Alerte pour les autres notifications + Alerte pour les notifications du calendrier + Alerte (vibre/bipe) pour les appels entrants + Alerte pour les appels entrants + Alerte pour les notifications d\'email + Alerte (vibre/bipe) pour les notifications d\'email + Alerte (vibre/bipe) pour les notifications SMS (message texte) + Montrer un aperçu du message dans le titre + Alerte (vibre/bipe) pour les notifications du calendrier + Alerte (vibre/bipe) pour les notifications de la catÊgorie autre + Nom de l\'app dans la notification + Alerte pour les notifications SMS + Mi Watch Color Sport + Applications de navigation + Xiaomi Smart Band 8 + Course à pieds + Planification de l\'heure de coucher + Heure du coucher + Pixoo + PrÊfÊrences de navigation + Focus + Google Maps + Ajouter un prÊfixe au titre de la notification avec le nom de l\'application source + Redmi Watch 3 Active + NumÊro de sÊrie + Danois + UtilisÊ pour sÊlectionner la version d\'OsmAnd à laquelle se connecter + RÊveil + Xiaomi Smart Band 7 Pro + Stats + Xiaomi Watch Lite + Afficher un aperçu du message dans le titre de la notification comme permis par l\'appareil + nom du paquet OsmAnd + OsmAnd(+) + Envoyer un rappel et se mettre en veille à l\'heure du coucher. A l\'heure prÊvue de rÊveil, l\'alarme de rÊveil sonnera. + Alertes + Xiaomi Watch S1 Active + Ne pas dÊranger - Actif + Objectif secondaire + Envoyer une notification quand votre score de vitalitÊ atteint 30, 60 ou 100 sur les 7 derniers jours + Score VitalitÊ + Ne pas dÊranger - Alarmes uniquement + Envoyer une notification lorsque vous atteignez le maximum de points quotidiens de vitalitÊ + Envoyer les notifications de l\'application à l\'appareil + Ne pas dÊranger - Prioritaire uniquement + Non configurÊ + Temps de veille + Temps actif + Envoyer des notifications + Progrès quotidien + Progrès sur 7 jours + Ne pas dÊranger - Off \ No newline at end of file diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 3d8cee9b5..7ec96a4ba 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -8,14 +8,14 @@ SzinkronizÃĄlÃĄs Elveszett eszkÃļz keresÊse KÊpernyőkÊp kÊszítÊse - Kapcsolat megszÃŧntetÊse + LekapcsolÃŗdÃĄs EszkÃļz tÃļrlÊse %1$s tÃļrlÊse Az eszkÃļz Ês az Ãļsszes hozzÃĄtartozÃŗ adat tÃļrlÊsre kerÃŧl! NavigÃĄciÃŗs fiÃŗk megnyitÃĄsa NavigÃĄciÃŗs fiÃŗk bezÃĄrÃĄsa - Nyomja hosszan a szÊtkapcsolÃĄshoz - SzÊtkapcsolÃŗdÃĄs + Nyomja hosszan a lekapcsolÃŗdÃĄshoz + LekapcsolÃŗdÃĄs KapcsolÃŗdÃĄsâ€Ļ KÊpernyőkÊp kÊszítÊse az eszkÃļzről HibakeresÊs @@ -51,12 +51,12 @@ ÁltalÃĄnos beÃĄllítÃĄsok KapcsolÃŗdjon a Gadgetbridge eszkÃļzhÃļz a Bluetooth bekapcsolÃĄsakor Automatikus indulÃĄs - Automatikus ÃējracsatlakozÃĄs + Automatikus ÃējrakapcsolÃŗdÃĄs PreferÃĄlt zenelejÃĄtszÃŗ AlapÊrtelmezett DÃĄtum Ês idő Idő szinkronizÃĄlÃĄsa - Idő szinkronizÃĄlÃĄsa a Gadgetbridge eszkÃļzzel csatlakozÃĄskor, Ês amikor az Android eszkÃļz ideje vagy időzÃŗnÃĄja vÃĄltozik + Idő szinkronizÃĄlÃĄsa a Gadgetbridge eszkÃļzzel kapcsolÃŗdÃĄskor, Ês amikor az Android eszkÃļz ideje vagy időzÃŗnÃĄja vÃĄltozik TÊma VilÃĄgos SÃļtÊt @@ -113,12 +113,12 @@ NormÃĄl ÊrtesítÊsek Az ÊrtesítÊsszÃļveg eltolÃĄsa kÊpernyőn Csak az ÊrtesítÊsi ikon megjelenítÊse - Helyzet - PoziciÃŗ meghatÃĄrozÃĄsa + Hely + Hely meghatÃĄrozÃĄsa SzÊlessÊg HosszÃēsÃĄg - Helyzet frissen tartÃĄsa - AktuÃĄlis poziciÃŗ meghatÃĄrozÃĄsÃĄnak megprÃŗbÃĄlÃĄsa futÃĄsidőben, hiba esetÊn az eltÃĄrolt pozíciÃŗ hasznÃĄlata + Hely frissen tartÃĄsa + AktuÃĄlis hely meghatÃĄrozÃĄsÃĄnak megprÃŗbÃĄlÃĄsa futÃĄsidőben, hiba esetÊn az eltÃĄrolt hely hasznÃĄlata KÊrem, engedÊlyezze a hÃĄlÃŗzati helymeghatÃĄrozÃĄst Hely meghatÃĄrozva ÉrtesítÊsi protokoll erőltetÊse @@ -130,18 +130,18 @@ Pebble 2/LE GATT MTU hatÃĄrÊrtÊk Ha a Pebble 2/Pebble LE nem mÅąkÃļdik elvÃĄrtan, prÃŗbÃĄlja beÃĄllítani az MTU hatÃĄrÊrtÊket (az ÊrvÊnyes tartomÃĄny: 20–512) ÓraalkalmazÃĄs-naplÃŗzÃĄs engedÊlyezÊse - A karÃŗra-alkalmazÃĄsok naplÃŗit a Gadgetbridge is naplÃŗzni fogja (ÃējracsatlakozÃĄst igÊnyel) + A karÃŗra-alkalmazÃĄsok naplÃŗit a Gadgetbridge is naplÃŗzni fogja (ÃējrakapcsolÃŗdÃĄst igÊnyel) Korai ACK PebbleKit Minden esetben azonnal felismeri az Ãŧzeneteket, amelyeket kÃŧlső harmadik alkalmazÃĄsoknak kÃŧldenek - ÚjracsatlakozÃĄsi kísÊrletek + ÚjrakapcsolÃŗdÃĄsi kísÊrletek EgysÊgek IdőformÃĄtum Kijelző bekapcsolva tartÃĄsÃĄnak ideje EgÊsz napos pulzusmÊrÊs HPlus/Makibes beÃĄllítÃĄsok - Nincs csatlakozva - CsatlakozÃĄs - Csatlakozva + Nincs kapcsolÃŗdva + KapcsolÃŗdÃĄs + KapcsolÃŗdva Ismeretlen ÃĄllapot (ismeretlen) Teszt @@ -149,11 +149,11 @@ Ez egy teszt ÊrtesítÊs a Gadgetbridge-től A Bluetooth nem tÃĄmogatott. A Bluetooth ki van kapcsolva. - Koppintson az eszkÃļzre az alkalmazÃĄskezelőhÃļz - Koppintson az eszkÃļzre az aktivitÃĄshoz - Koppintson az eszkÃļzre a rezgetÊshez - Koppintson az eszkÃļzre a csatlakozÃĄshoz - Nem lehet csatlakozni. Lehet, hogy ÊrvÊnytelen a Bluetooth cím\? + Koppintson a kapcsolÃŗdott eszkÃļzre az alkalmazÃĄskezelőhÃļz + Koppintson a kapcsolÃŗdott eszkÃļzre az aktivitÃĄshoz + Koppintson a kapcsolÃŗdott eszkÃļzre a rezgetÊshez + Koppintson az eszkÃļzre a kapcsolÃŗdÃĄshoz + Nem lehet kapcsolÃŗdni. Lehet, hogy ÊrvÊnytelen a Bluetooth cím? A Gadgetbridge fut %1$d/%2$d binÃĄris telepítÊse Sikertelen telepítÊs @@ -170,10 +170,10 @@ N/A inicializÃĄlt %1$s , tőle: %2$s - EszkÃļz keresÊse + EszkÃļz felderítÊse KeresÊs leÃĄllítÃĄsa - KeresÊs indítÃĄsa - Új eszkÃļz csatlakoztatÃĄsa + FelderítÊs indítÃĄsa + Új eszkÃļz kapcsolÃŗdÃĄsa %1$s (%2$s) EszkÃļz pÃĄrosítÃĄsa HasznÃĄlja az Android Bluetooth pÃĄrosítÃĄs ablakot eszkÃļz pÃĄrosítÃĄsÃĄhoz. @@ -182,7 +182,7 @@ ÖsszekÃļttetÊs lÊtesítÊse ezzel: %1$s (%2$s) Sikertelen pÃĄrosítÃĄs ezzel: %1$s (%2$s) ÖsszekÃļttetÊs folyamatban ezzel: %1$s (%2$s) - %1$s (%2$s) mÃĄr pÃĄrosítva van, kapcsolÃŗdÃĄsâ€Ļ + %1$s (%2$s) mÃĄr Ãļssze van kÃļttetve, kapcsolÃŗdÃĄsâ€Ļ Nincs talÃĄlat a MAC címre, nem lehet pÃĄrosítani. EszkÃļzspecifikus beÃĄllítÃĄsok MI Band / Amazfit beÃĄllítÃĄsok @@ -194,7 +194,7 @@ ÉrvÊnytelen felhasznÃĄlÃŗi adatok vannak megadva, alapÊrtelmezett adatok hasznÃĄlata. Amikor rezegni kezd a Mi Band-je, Êrintse meg pÃĄrszor egymÃĄs utÃĄn. TelepítÊs - Tegye lÃĄthatÃŗvÃĄ a kÊszÃŧlÊkÊt. A jelenleg csatlakoztatott kÊszÃŧlÊkek nem fognak lÃĄthatÃŗkÊnt megjelenni. AktivÃĄlja a helymeghatÃĄrozÃĄst (Pl: GPS) az Android 6+ kÊszÃŧlÊkÊn. Kapcsolja ki a Privacy Guard-ban a Gadgetbridge-et (Ha van Privacy Guard beÃĄllítÃĄsa), kÃŧlÃļnben az alkalmazÃĄs Ãļsszeomolhat, illetve Ãējraindulhata kÊszÃŧlÊke. Ha nem talÃĄlhatÃŗ az eszkÃļz pÃĄr percen belÃŧl, prÃŗbÃĄlja meg Ãējraindítani a telefonjÃĄt. + Tegye felderíthetővÊ a kÊszÃŧlÊkÊt. A jelenleg kapcsolÃŗdott kÊszÃŧlÊkek nem fognak felderíthetőkÊnt megjelenni. AktivÃĄlja a helymeghatÃĄrozÃĄst (Pl: GPS) az Android 6+ kÊszÃŧlÊkÊn. Kapcsolja ki a Privacy Guard-ban a Gadgetbridge-et (Ha van Privacy Guard beÃĄllítÃĄsa), kÃŧlÃļnben az alkalmazÃĄs Ãļsszeomolhat, illetve Ãējraindulhat a telefonja. Ha nem talÃĄlhatÃŗ az eszkÃļz pÃĄr percen belÃŧl, prÃŗbÃĄlja meg Ãējraindítani a telefonjÃĄt. MegjegyzÊs: EszkÃļzkÊp NÊv/ÃĄlnÊv @@ -266,8 +266,8 @@ %1$s: %2$s %3$s Kompatibilis verziÃŗ Nem tesztelt verziÃŗ! - CsatlakozÃĄs az eszkÃļzhÃļz: %1$s - Pebble Firmware %1$s + KapcsolÃŗdÃĄs az eszkÃļzhÃļz: %1$s + Pebble firmware %1$s Helyes hardververziÃŗ HardververziÃŗ eltÊrÊs! %1$s (%2$s) @@ -298,7 +298,7 @@ KÃļnnyÅą alvÃĄs MÊly alvÃĄs Nem viselt - Nincs csatlakoztatva. + Nincs kapcsolÃŗdva. Mindegyik ÊbresztÊs letiltva AktivitÃĄsadatok eszkÃļzÃļn hagyÃĄsa Nem kompatibilis firmware @@ -324,7 +324,7 @@ Kezdete VÊge AdatÃĄtvitelre kÊszÃŧl: %1$s Ãŗta - VÃĄrakozÃĄs az ÃējracsatlakozÃĄsra + VÃĄrakozÃĄs az ÃējrakapcsolÃŗdÃĄsra Önről SzÃŧletÊsi Êv Nem @@ -345,10 +345,17 @@ Pulzus Pulzus Nyers adatok tÃĄrolÃĄsa az adatbÃĄzisban - Ha bejelÃļlÃļd, az adatok eredeti formÃĄban lesznek tÃĄrolva kÊsőbbi ÊrtelmezÊshez. Megj.: az adatbÃĄzis ebben az esetben nagyobb lesz! + Az adatokat eredeti formÃĄban tÃĄrolja, nÃļvelve az adatbÃĄzishasznÃĄlatot a kÊsőbbi feldolgozÃĄs lehetővÊ tÊtele ÊrdekÊben. AdatkezelÊs AdatkezelÊs - Az adatbÃĄzismÅąvelet ezt a helyet fogja hasznÃĄlni. Ez a hely elÊrhető mÃĄsik Android-alkalmazÃĄsok Ês a szÃĄmítÃŗgÊp szÃĄmÃĄra. Itt keresd az exportÃĄlt adatbÃĄzist is, illetve ide rakd az importÃĄlni kívÃĄnt adatbÃĄzist is. + Az exportÃĄlÃĄs/importÃĄlÃĄs mÅąveletek a kÃļvetkező kÃļnyvtÃĄr ÃētvonalÃĄt (lÃĄsd lentebb) hasznÃĄljÃĄk az eszkÃļzÊn. Ez a kÃļnyvtÃĄr elÊrhető mÃĄs Android alkalmazÃĄsok Ês a szÃĄmítÃŗgÊpe szÃĄmÃĄra. Jegyezze meg, hogy ez a kÃļnyvÃĄr Ês az Ãļsszes tartalmazott fÃĄjl tÃļrÃļlve lesz, ha eltÃĄvolítja a Gadgetbridge-et. Az adatok tartalmazzÃĄk: +\n Export_preference - globÃĄlis beÃĄllítÃĄsok +\n Export_preference_MAC - eszkÃļzspecifikus beÃĄllítÃĄsok +\n Gadgetbridge - eszkÃļz- Ês aktivitÃĄs-adatbÃĄzis +\n Gadgetbridge_date - egy dÃĄtumon exportÃĄlt adatbÃĄzis +\n *.gpx - GPS feljegyzÊsek +\n *.log - naplÃŗfÃĄjlok +\nVÃĄrhatÃŗan itt megtalÃĄlja az exportÃĄlt fÃĄjlokat (vagy helyezze ide az importÃĄlni kívÃĄnt fÃĄjlokat): RÊgi adatbÃĄzis tÃļrlÊse Nem elÊrhető az exportÃĄlÃĄsi Ãētvonal. KÊrjÃŧk vegye fel a kapcsolatot a fejlesztőkkel. ExportÃĄlva ide: %1$s @@ -377,7 +384,7 @@ Egy pÃĄrosítÃĄsi pÃĄrbeszÊdablak fog megjelennie az Android eszkÃļzÊn. Ha ez nem tÃļrtÊnik meg, nÊzze meg az ÊrtesítÊsi sÃĄvban Ês fogadja el a pÃĄrosítÃĄsi kÊrelmet. UtÃĄna fogadja el a kÊrelmet a Pebble-n is. Bizonyosodjon meg rÃŗla, hogy a skin tÃĄmogatja-e az IdőjÃĄrÃĄs ÊrtesítÊsek megjelenítÊsÊt. Nincs szÃŧksÊg konfigurÃĄciÃŗra. A Pebble app management-ben tudja engedÊlyezni a rendszer időjÃĄrÃĄs alkalmazÃĄsÃĄt. Azok a skinek amik tÃĄmogatjÃĄk az időjÃĄrÃĄs ÊrtesítÊsket, automatikusan megfogjÃĄk jeleníteni azt. Bluetooth pÃĄrosítÃĄs engedÊlyezÊse - Kapcsolja ki ezt, ha gondja van a csatlakozÃĄssal + Kapcsolja ki ezt, ha gondja van a kapcsolÃŗdÃĄssal Metrikus Birodalmi 24H @@ -394,16 +401,16 @@ Automatikus (alvÃĄsÊrzÊkelÊs) Időzített (idő intervallum) PÃĄrosítÃĄs megkísÊrlÊse ezzel: %1$s - A csatlakozÃĄs a(z) %1$s eszkÃļzzel azonnal megszakadt. - CsatlakozÃĄs megkísÊrlÊse ezzel: %1$s + Az ÃļsszekÃļttetÊs a(z) %1$s eszkÃļzzel azonnal megszakadt. + KapcsolÃŗdÃĄs megkísÊrlÊse ezzel: %1$s EngedÊlyezze a Bluetooth-ot az eszkÃļzÃļk felderítÊsÊhez. - PÃĄrosítva ehhez: %1$s. - %1$s pÃĄrosítÃĄsa\? + ÖsszekÃļttetve ezzel: %1$s. + %1$s pÃĄrosítÃĄsa? VÃĄlassza ki a pÃĄrosítÃĄst az eszkÃļz pÃĄrosítÃĄsÃĄhoz. Ha ez nem sikerÃŧl, prÃŗbÃĄlja meg Ãējra pÃĄrosítÃĄs nÊlkÃŧl. PÃĄrosítÃĄs Ne pÃĄrosítsa AdomÃĄnyozÃĄs - CsatlakoztatÃĄsâ€Ļ + KapcsolÃŗdÃĄsâ€Ļ IdőjÃĄrÃĄs AktivitÃĄsadatok automatikus lekÊrdezÊse Vízszintes @@ -455,9 +462,9 @@ %1$ akkumulÃĄtora merÃŧl: %2$s A gyÃĄri beÃĄllítÃĄsok visszaÃĄllítÃĄsa minden adatot tÃļrÃļl a kapcsolÃŗdott eszkÃļzről (ha tÃĄmogatott). A Xiaomi/Huami eszkÃļzÃļknek emellett megvÃĄltozik a Bluetooth MAC-címe is, így Ãēj eszkÃļzkÊnt jelennek meg a Gadgetbridge szÃĄmÃĄra. nodomain.freeyourgadget.gadgetbridge.ButtonPressed - EgÊsz napos szívritmus mÊrÊs + EgÊsz napos pulzusmÊrÊs KÊpernyő tÃĄjolÃĄsa - NaptÃĄr ÊrtesítÊsek bekapcsolÃĄsa mikor nincs kapcsolat a telefonnal + NaptÃĄr ÊrtesítÊsek bekapcsolÃĄsa, akkor is, ha le van kapcsolÃŗdva a telefonrÃŗl Ön a %s firmware-t telepíti az Amazfit Cor-ra. \n \nGyőződjÃļn meg, hogy a .fw, Ês vÊgÃŧl a .res fÃĄjl is telepítÊsre kerÃŧl! Az Ãŗra a .fw file telepítÊse utÃĄn Ãējraindul. @@ -568,7 +575,7 @@ JelzÊs típusÃĄnak beÃĄllítÃĄsa az ÊbresztőhÃļz Ébresztő beÃĄllítÃĄsa ennyi idő mÃēlva: ÉbresztÊs - Nincs csatlakozva, Êbresztő nincs beÃĄllítva. + Nincs kapcsolÃŗdva, Êbresztő nincs beÃĄllítva. Kezek Ês lÊpÊsek Megvan! AdatbÃĄzis ÃŧrítÊse @@ -633,7 +640,7 @@ Biztosan tÃļrÃļlni szeretne %d aktivitÃĄst\? 7 nap SportaktivitÃĄsok - VoIP alkalmazÃĄshívÃĄsok engedÊlyezÊse + VoIP alkalmazÃĄs hívÃĄsainak engedÊlyezÊse NaptÃĄrak feketelistÃĄzÃĄsa ExportÃĄlÃĄs helye Automatikus exportÃĄlÃĄs engedÊlyezve @@ -645,7 +652,7 @@ Gyors ÊbresztÊs 1 Ãŗra Napi cÊl: zsírÊgetÊs ideje percben - Gadgetbridge Nightly + Gadgetbridge nightly 5 perc Automatikus %1$s, minden nap @@ -658,7 +665,7 @@ KÊpernyő bekapcsolva tartÃĄsa edzÊs kÃļzben Tegnap Mi Band 3 - Gadgetbridge Nightly Pebble nÊlkÃŧl + Gadgetbridge nightly Pebble nÊlkÃŧl GPS kÃŧldÊse edzÊs kÃļzben Összes edzÊs 6.3k @@ -666,7 +673,7 @@ ÉbresztÊs EdzÊs A Gadgetbridge frissítÊse utÃĄni vÃĄltozÃĄsnaplÃŗ megjelenítÊse az utolsÃŗ verziÃŗ Ãŗta - About Gadgetbridge Nightly Pebble nÊlkÃŧl nÊvjegye + Gadgetbridge nightly Pebble nÊlkÃŧl nÊvjegye Napi cÊl: ÃĄllÃĄsi idő percben Automatikus Watch X Plus kalibrÃĄciÃŗ @@ -683,13 +690,13 @@ %d Ãŗra %d Ãŗra - Gadgetbridge Nightly nÊvjegye + Gadgetbridge nightly nÊvjegye Egy karkÃļtő UtolsÃŗ edzÊs VÃĄltozÃĄsnaplÃŗ megjelenítÊse indulÃĄskor NYERS adatok megjelenítÊse az aktivitÃĄsgrafikonon ÚjdonsÃĄgok - Bangle.js Gadgetbridge (Nightly) nÊvjegye + Bangle.js Gadgetbridge (nightly) nÊvjegye 3 Ãŗra Minimum pulzus Tegnapi aktivitÃĄs @@ -702,7 +709,7 @@ Napi cÊl: tÃĄvolsÃĄg mÊterben Összesen %d lÊpÊs naponta KÊrdezzen előszÃļr - Bangle.js Gadgetbridge (Nightly) + Bangle.js Gadgetbridge (nightly) 1k ÉszlelÊs engedÊlyezÊse Amazfit Band 5 @@ -897,7 +904,7 @@ Jelenítse meg, amikor ezeket a szavakat tartalmazza Naplementekor Pulzusadatok lekÊrdezÊse - EszkÃļz csatlakoztatva + EszkÃļz kapcsolÃŗdva TradicionÃĄlis kínai MyKronoz ZeTime sec/m @@ -930,7 +937,7 @@ Minimum percenkÊnti lÊpÊs futÃĄs ÊszlelÊsÊhez InaktivitÃĄsi ÊrtesítÊs a vÃĄltoztatÃĄs eltarthat nÊhÃĄny mÃĄsodpercigâ€Ļ - Az Ãŗra nincs csatlakozva + Az Ãŗra nincs kapcsolÃŗdva Havi alvÃĄs CsÃļkkenő KÃļnnyÅą alvÃĄs @@ -1074,8 +1081,8 @@ SebessÊg előszÃļr UM-25 BFH-16 - Bangle.js Gadgetbridge (Nightly) - Gadgetbridge (Nightly) + Bangle.js Gadgetbridge (nightly) + Gadgetbridge (nightly) LÊpÊsek MÃĄszÃĄs TÃĄvolsÃĄg @@ -1190,7 +1197,7 @@ Cím SMA-Q2 OSS +%d - MÁR PÁROSÍTVA VAN + MÁR ÖSSZE VAN KÖTTETVE Előző szÃĄm ErősítÊs KÃŗd @@ -1248,9 +1255,300 @@ 1 lÊpÊs Az alkalmazÃĄs letÃļltÊse elindult EnergiatakarÊkos - Bangle.js jelenleg fut - Ez a mÅąvelet az Ãļsszes kapcsolt eszkÃļz beÃĄllítÃĄsÃĄt alap helyzetbe helyezi. Biztos benne\? + A Bangle.js fut + Ez a mÅąvelet az Ãļsszes kapcsolÃŗdott eszkÃļz beÃĄllítÃĄsÃĄt alap helyzetbe helyezi. Biztos benne? GPS + GLONASS GPS + BDS Alacsony energiÃĄjÃē GPS + 110 bpm + 135 bpm + MÊrÊsek gyakorisÃĄga + 105 bpm + 40 bpm + Pulzus riasztÃĄs + 130 bpm + Pulzus beÃĄllítÃĄsok + Jelenlegi / maximum pulzus: %1$d / %2$d + PulzusmÊrÊs beÃĄllítÃĄsa + EgÊsz napos mÊrÊs + PulzusmÊrÊs + Pulzus riasztÃĄsok + 125 bpm + 100 bpm + 145 bpm + 115 bpm + 150 bpm + Automatikus pulzusmÊrÊsek + 140 bpm + AlvÃĄs kÃļzben vÊgezzen mÊrÊseket + 45 bpm + 120 bpm + Pulzus riasztÃĄs engedÊlyezÊse + 50 bpm + A helymeghatÃĄrozÃĄsnak engedÊlyezve kell lennie + HÃĄlÃŗzat hasznÃĄlata csak a hely meghatÃĄrozÃĄsÃĄhoz + GPS hely kÃŧldÊse %1$d eszkÃļzÃļkre + Tiszta fehÊr + Átlagos pulzus + MÃŗdosítsa a hitelesítÊsi kulcsot egy kÃļzÃļs kulcsra az Ãļsszes olyan Android-eszkÃļzÊn, amelyről kapcsolÃŗdni szeretne. A korÃĄbbi alapÊrtelmezett kulcs minden eszkÃļzÃļn: 0123456789@ABCDE + HÃĄromszor + SebessÊg vÃĄrosa + Telefon nÊma mÃŗd + MÃĄsodik időzÃŗna widget + IdőjÃĄrÃĄs-informÃĄciÃŗ gyorsítÃŗtÃĄrazÃĄsa + RÃļvid + Az eszkÃļz kapcsolÃŗdik + Pulzus szín + Bluetooth keresÊs: + Szabad kombinÃĄciÃŗ + Az automatikus exportÃĄlÃĄs engedÊlyezve van. + FÃĄjlok tÃļrlÊse az exportÃĄlÃĄs/importÃĄlÃĄs kÃļnyvtÃĄrbÃŗl + Okos rezgÊs + Üzleti stílus + Egy szÃĄmlap ezzel a nÊvvel mÃĄr lÊtezik a gyorsítÃŗtÃĄrban. SzeretnÊ felÃŧlírni? + KÃļzepes + Smaragd holdfÊny + ExportÃĄlÃĄs/importÃĄlÃĄs kÃļnyvtÃĄr tartalma + NormÃĄl / rezgÊs + LekapcsolÃŗdÃĄsi ÊrtesítÊs + Folyamatos + A kapcsolÃŗdÃĄs nincs megerősítve az ÃŗrÃĄn, hitelesítÊs nÊlkÃŧli mÃŗd hasznÃĄlata + ÉlÊnk + Az automatikus exportÃĄlÃĄs nincs időzítve. + Az exportÃĄlt fÃĄjlok az exportÃĄlÃĄs/importÃĄlÃĄs kÃļnyvtÃĄrban bÃĄrmilyen alkalmazÃĄs szÃĄmÃĄra elÊrhetőek az eszkÃļzÊn. Lehet, hogy tÃļrÃļlni szeretnÊ ezeket a fÃĄjlokat szinkronizÃĄlÃĄs vagy biztonsÃĄgi mentÊs utÃĄn. GyőződjÃļn meg, hogy van mentÊse, mielőtt tÃļrli őket. A GPX fÃĄjlok, az alkÃļnyvtÃĄrak Ês az automatikusan exportÃĄlt adatbÃĄzis-fÃĄjlok (ha lÊteznek) nem lesznek tÃļrÃļlve. Az exportÃĄlÃĄs/importÃĄlÃĄs kÃļnyvtÃĄr Ãētvonala: + Gyenge + Az automatikus exportÃĄlÃĄs nincs engedÊlyezve. + RezgÊsmintÃĄk beÃĄllítÃĄsa a kÃŧlÃļnbÃļző ÊrtesítÊsekhez + Csillagos Êg + A beírt titkos hitelesítÊsi kulcs ÊrvÊnytelen! Nyomjon hosszan az eszkÃļzre a szerkesztÊshez. + ExportÃĄlÃĄs/importÃĄlÃĄs kÃļnyvtÃĄr tartalmÃĄnak megjelenítÊse + RezgÊs hívÃĄsokhoz, Ãŧzenetekhez, ÊrtesítÊsekhez Ês tÃļbbhÃļz + VÃļrÃļs fantÃĄzia + Pulzus riasztÃĄs sport aktivitÃĄs kÃļzben + Az eszkÃļz lekapcsolÃŗdik! + Alap + TÃļrli a fÃĄjlokat az exportÃĄlÃĄs/importÃĄlÃĄs kÃļnyvtÃĄrbÃŗl? + KÊtszer + Ennek az eszkÃļznek szÃŧksÊge van egy titkos hitelesítÊsi kulcsra, nyomjon hosszan az eszkÃļzre, hogy be tudja írni. Olvassa ez a wikit. + Biztosan tÃļrli a fÃĄjlokat az exportÃĄlÃĄs/importÃĄlÃĄs kÃļnyvtÃĄrbÃŗl? + Gyors + Sok kÃļszÃļnet minden fel nem sorolt kÃļzremÅąkÃļdőnek, akik kÃļzremÅąkÃļdtek ezekben: kÃŗdok, fordítÃĄsok, tÃĄmogatÃĄs, Ãļtletek, motivÃĄciÃŗ, hibajelentÊsek, pÊnzâ€Ļ ✊ + RezgÊs / nÊma + Egyszer + LÃĄthatÃŗ, amíg kapcsolÃŗdva van + Pulzus zÃŗnÃĄk + MentÊs Ês alkalmazÃĄs + BejÃļvő hívÃĄs rezgÊs + Az időjÃĄrÃĄs-informÃĄciÃŗ gyorsítÃŗtÃĄrazva lesz az alkalmazÃĄs-ÃējraindítÃĄsok kÃļzÃļtt. + Hatalmas Êg + A gyÃĄrtÃŗk zÃĄrt forrÃĄskÃŗdÃē Android-kÃŧtyÃŧalkalmazÃĄsainak felhő nÊlkÃŧli, szabad, copyleft helyettesítÊse. A Gadgetbridge nightly kiadÃĄsa. Ebben a verziÃŗban a Pebble szolgÃĄltatÃŗt ÃĄtneveztÊk az ÃŧtkÃļzÊsek elkerÃŧlÊse ÊrdekÊben, így egyes Pebble-lel kapcsolatos integrÃĄciÃŗk nem mÅąkÃļdnek, de telepíthető a meglÊvő Gadgetbridge-telepítÊs mellÊ. + UtolsÃŗ automatikus exportÃĄlÃĄs: %1$s + EgyszerÅą adatok + Új hitelesítÊsi protokoll + SzÃĄmlap konfigurÃĄciÃŗ + GyorsítÃŗtÃĄrazÃĄs, amíg hatÃŗtÃĄvon kívÃŧl van + Bluetooth LE keresÊs: + Az eszkÃļz lekapcsolÃŗdott! + Legyen pulzusmÊrÊs + VillÃĄmlÃĄs + SzirÊna + Ennek az opciÃŗnak az engedÊlyezÊse ignorÃĄlni fogja keresÊskor azokat az eszkÃļzÃļket, amik mÃĄr ÃļsszekÃļttetve/pÃĄrosítva vannak + Automatikus exportÃĄlÃĄs futtatÃĄsa most + BejÃļvő hívÃĄs rezgÊs ismÊtlÊs + ÉrtesítÊs rezgÊs + Folyamatosan + FelderítÊsi Ês pÃĄrosítÃĄsi beÃĄllítÃĄsok + NÊgyszer + Kihagyott ÊrtesítÊsek elkÃŧldÊse, amikor az eszkÃļz ÃējrakapcsolÃŗdik, miutÃĄn hatÃŗtÃĄvon kívÃŧl volt + RezgÊs erőssÊge + Az Ãŗra figyelmeztetni fog, amikor a pulzus ÃĄtlÊpi a hatÃĄrokat. + A karkÃļtő rezegni fog, ha a telefon lekapcsolÃŗdik a karkÃļtőről + Sok adat + A gyÃĄrtÃŗk zÃĄrt forrÃĄskÃŗdÃē Android-kÃŧtyÃŧalkalmazÃĄsainak felhő nÊlkÃŧli, szabad, copyleft helyettesítÊse. A Gadgetbridge nightly kiadÃĄsa. Nem telepíthető, ha mÃĄr telepítve van a Gadgetbridge vagy a Pebble alkalmazÃĄs, a Pebble szolgÃĄltatÃŗ ÃŧtkÃļzÊse miatt. + ÖsszekÃļttetett eszkÃļzÃļk ignorÃĄlÃĄsa + ÉrtesítÊs rezgÊs ismÊtlÊs + ForgÃŗ fÃļld + Kommit %s + EngedÊlyezze ezt az opciÃŗt, ha az eszkÃļze nem talÃĄlhatÃŗ felderítÊs sorÃĄn + Nem elÊrhető hitelesítÊs nÊlkÃŧli mÃŗdban + NormÃĄl / nÊma + FigyelmeztetÊs + A hitelesítÊs sikertelen, korlÃĄtozott funkcionalitÃĄs + Ha \"Frissítse az alkalmazÃĄst a legutÃŗbbi verziÃŗra\" Ãŧzenetet kap, győződjÃļn meg rÃŗla, hogy engedÊlyezte az \"Új hitelesítÊsi protokoll\" opciÃŗt fentebb. Koppintson ide a tovÃĄbbi rÊszletekÊrt a wikiben. + Nincs rezgÊs + Erős + HÃĄromszor + NÊhÃĄny eszkÃļznek szÃŧksÊge van egy kÃŧlÃļnleges pÃĄrosítÃĄsi kulcsra az első alkalommal. Koppintson ide a tovÃĄbbi rÊszletekÊrt a wikiben. + Android tÃĄrsalkalmazÃĄs a Bangle.js szÃĄmÃĄra, amely a Gadgetbridge projektre ÊpÃŧl, hozzÃĄadott internet-hozzÃĄfÊrÊssel. +\n +\nA Google Play ÁruhÃĄz szabÃĄlyzata miatt nem lehet az adomÃĄnyozÃĄsi link magÃĄban az alkalmazÃĄsban, de ha tetszik az alkalmazÃĄs, kÊrjÃŧk, fontolja meg az adomÃĄnyozÃĄst az alÃĄbbi Gadgetbridge honlapon keresztÃŧl. + Gyors elÊrÊs (hÃĄrom ÊrintÊs) + Sony WH-1000XM2 + A nightly GB fut + Gadgetbridge (nightly, Pebble szolgÃĄltatÃŗ nÊlkÃŧl) + a nightly Pebble nÊlkÃŧli GB fut + BeltÊri kerÊkpÃĄrozÃĄs + Sony WH-1000XM5 + A nightly Bangle.js fut + BeltÊri fitnesz + Nem fut + sÊta felismerÊse + evezÊs felismerÊse + BeltÊri jÊgkorcsolyÃĄzÃĄs + JÊgkorcsolyÃĄzÃĄs + Sony LinkBuds S + BeltÊri sÊta + Gyors elÊrÊs (kettő ÊrintÊs) + Sony WF-1000XM4 + Sony WF-1000XM5 + FutÃĄs + kerÊkpÃĄrozÃĄs felismerÊse + Pebble (cipőcsat) + Ez csak a Pebble 2 szÃĄmÃĄra kÊszÃŧlt Ês kísÊrleti, prÃŗbÃĄlja ezt, ha kapcsolÃŗdÃĄsi problÊmÃĄi vannak. + futÃĄs felismerÊse + Android tÃĄrsalkalmazÃĄs a Bangle.js szÃĄmÃĄra, amely a Gadgetbridge projektre ÊpÃŧl, hozzÃĄadott internet-hozzÃĄfÊrÊssel. +\n +\nA Google Play ÁruhÃĄz szabÃĄlyzata miatt nem lehet az adomÃĄnyozÃĄsi link magÃĄban az alkalmazÃĄsban, de ha tetszik az alkalmazÃĄs, kÊrjÃŧk, fontolja meg az adomÃĄnyozÃĄst az alÃĄbbi Gadgetbridge honlapon keresztÃŧl. + KÃŧltÊri futÃĄs + MegnyitÃĄs Android eszkÃļzÃļn + KÃŧltÊri kerÊkpÃĄrozÃĄs + HÃĄttÊrbeli JS engedÊlyezÊse + Sony Wena 3 + KÃŧltÊri színpad + Az adatbÃĄzis exportÃĄlÃĄsa meghiÃēsult. Ellenőrizze a beÃĄllítÃĄsait! + Ön telepíteni kívÃĄnja a %s firmware-t az Amazfit Cor 2 kÊszÃŧlÊkÊre. +\n +\nKÊrjÃŧk, győződjÃļn meg rÃŗla, hogy a .fw fÃĄjlt, majd ezt kÃļvetően a .res fÃĄjlt is telepíti. A karkÃļtő a .fw fÃĄjl telepítÊse utÃĄn Ãējraindul. +\n +\nMegjegyzÊs: Nem kell telepítenie a .res fÃĄjlt, ha az pontosan megegyezik a korÃĄbban telepítettel. +\n +\nFOLYTATÁS CSAK SAJÁT FELELŐSSÉGRE! +\n +\nTELJESEN TESZTELETLEN, VALÓSZÍNÅ°LEG A BEATS_W FIRMWARE-t kell feltÃļltenie, HA A KÉSZÜLÉK NEVE \"Amazfit Band 2\" + Ön telepíteni kívÃĄnja a %s firmware-t az Amazfit GTS kÊszÃŧlÊkÊre. +\n +\nKÊrjÃŧk, győződjÃļn meg rÃŗla, hogy telepíti a .fw fÃĄjlt, a .res fÃĄjlt, vÊgÃŧl pedig a .gps fÃĄjlt. Az Ãŗra a .fw fÃĄjl telepítÊse utÃĄn Ãējraindul. +\n +\nMegjegyzÊs: Nem kell telepítenie a .res Ês .gps fÃĄjlokat, ha azok pontosan megegyeznek a korÃĄbban telepítettel. +\n +\nFOLYTATÁS CSAK SAJÁT FELELŐSSÉGRE! + Ön telepíteni kívÃĄnja a %s firmware-t az Amazfit Verge Lite kÊszÃŧlÊkÊre. +\n +\nKÊrjÃŧk, győződjÃļn meg rÃŗla, hogy telepíti a .fw fÃĄjlt, a .res fÃĄjlt, vÊgÃŧl pedig a .gps fÃĄjlt. Az Ãŗra a .fw fÃĄjl telepítÊse utÃĄn Ãējraindul. +\n +\nMegjegyzÊs: Nem kell telepítenie a .res Ês .gps fÃĄjlokat, ha azok pontosan megegyeznek a korÃĄbban telepítettel. +\n +\nFOLYTATÁS CSAK SAJÁT FELELŐSSÉGRE! + Ön telepíteni kívÃĄnja a %s firmware-t az Amazfit GTS 4 kÊszÃŧlÊkÊre. +\n +\nA .zip fÃĄjl telepítÊse utÃĄn a karkÃļtője Ãējraindul. +\n +\nFOLYTATÁS CSAK SAJÁT FELELŐSSÉGRE! + Ön telepíteni kívÃĄnja a %s firmware-t az Mi Band 5 kÊszÃŧlÊkÊre. +\n +\nKÊrjÃŧk, győződjÃļn meg rÃŗla, hogy előszÃļr a .fw fÃĄjlt telepítse, utÃĄna pedig a .res fÃĄjlt. A karkÃļtő a .fw fÃĄjl telepítÊse utÃĄn Ãējraindul. +\n +\nMegjegyzÊs: Nem kell telepítenie a .res Ês .gps fÃĄjlokat, ha azok pontosan megegyeznek a korÃĄbban telepítettel. +\n +\nFOLYTATÁS CSAK SAJÁT FELELŐSSÉGRE! + Ön telepíteni kívÃĄnja a %s firmware-t az Mi Band 6 kÊszÃŧlÊkÊre. +\n +\nKÊrjÃŧk, győződjÃļn meg rÃŗla, hogy előszÃļr a .fw fÃĄjlt telepítse, utÃĄna pedig a .res fÃĄjlt. A karkÃļtő a .fw fÃĄjl telepítÊse utÃĄn Ãējraindul. +\n +\nMegjegyzÊs: Nem kell telepítenie a .res Ês .gps fÃĄjlokat, ha azok pontosan megegyeznek a korÃĄbban telepítettel. +\n +\nFOLYTATÁS CSAK SAJÁT FELELŐSSÉGRE! + Ön telepíteni kívÃĄnja a %s firmware-t az Amazfit GTS 3 kÊszÃŧlÊkÊre. +\n +\nA .zip fÃĄjl telepítÊse utÃĄn a karkÃļtője Ãējraindul. +\n +\nFOLYTATÁS CSAK SAJÁT FELELŐSSÉGRE! + Ön telepíteni kívÃĄnja a %s firmware-t az Amazfit GTR 3 PRO kÊszÃŧlÊkÊre. +\n +\nA .zip fÃĄjl telepítÊse utÃĄn a karkÃļtője Ãējraindul. +\n +\nFOLYTATÁS CSAK SAJÁT FELELŐSSÉGRE! + Ön telepíteni kívÃĄnja a %s firmware-t az Amazfit GTR 4 kÊszÃŧlÊkÊre. +\n +\nA .zip fÃĄjl telepítÊse utÃĄn a karkÃļtője Ãējraindul. +\n +\nFOLYTATÁS CSAK SAJÁT FELELŐSSÉGRE! + Ön telepíteni kívÃĄnja a %s firmware-t az Amazfit T-Rex 2 kÊszÃŧlÊkÊre. +\n +\nA .zip fÃĄjl telepítÊse utÃĄn a karkÃļtője Ãējraindul. +\n +\nFOLYTATÁS CSAK SAJÁT FELELŐSSÉGRE! + Ön telepíteni kívÃĄnja a %s firmware-t az Amazfit Bip 3 Pro kÊszÃŧlÊkÊre. +\n +\nKÊrjÃŧk, győződjÃļn meg rÃŗla, hogy a .fw fÃĄjlt, majd ezt kÃļvetően a .res fÃĄjlt is telepíti. Az ÃŗrÃĄja az .fw fÃĄjl telepítÊse utÃĄn Ãējraindul. +\n +\nMegjegyzÊs: Nem kell telepítenie a .res fÃĄjlt, ha az pontosan megegyezik a korÃĄbban telepítettel. +\n +\nFOLYTATÁS CSAK SAJÁT FELELŐSSÉGRE! + Ön telepíteni kívÃĄnja a %s firmware-t az Amazfit X kÊszÃŧlÊkÊre. +\n +\nKÊrjÃŧk győződjÃļn meg rÃŗla, hogy előszÃļr telepítse a .fw fÃĄjlt, majd ezt kÃļvetően a .res fÃĄjlt. A .fw fÃĄjl telepítÊse utÃĄn a karkÃļtője Ãējraindul. +\n +\nMegjegyzÊs: Nem kell telepítenie a .res fÃĄjlt, ha az pontosan ugyanaz, mint a korÃĄbban telepített. +\n +\nFOLYTATÁS CSAK SAJÁT FELELŐSSÉGRE! + GPS + GALILEO + Ön telepíteni kívÃĄnja a %s firmware-t az Mi Band 3 kÊszÃŧlÊkÊre. +\n +\nKÊrjÃŧk, győződjÃļn meg rÃŗla, hogy előszÃļr a .fw fÃĄjlt telepítse, utÃĄna pedig a .res fÃĄjlt. A karkÃļtő a .fw fÃĄjl telepítÊse utÃĄn Ãējraindul. +\n +\nMegjegyzÊs: Nem kell telepítenie a .res Ês .gps fÃĄjlokat, ha azok pontosan megegyeznek a korÃĄbban telepítettel. +\n +\nFOLYTATÁS CSAK SAJÁT FELELŐSSÉGRE! + Ön telepíteni kívÃĄnja a %s firmware-t az Amazfit GTR kÊszÃŧlÊkÊre. +\n +\nKÊrjÃŧk, győződjÃļn meg rÃŗla, hogy telepíti a .fw fÃĄjlt, a .res fÃĄjlt, vÊgÃŧl pedig a .gps fÃĄjlt. Az Ãŗra a .fw fÃĄjl telepítÊse utÃĄn Ãējraindul. +\n +\nMegjegyzÊs: Nem kell telepítenie a .res Ês .gps fÃĄjlokat, ha azok pontosan megegyeznek a korÃĄbban telepítettel. +\n +\nFOLYTATÁS CSAK SAJÁT FELELŐSSÉGRE! + Ön telepíteni kívÃĄnja a %s firmware-t az Amazfit T-Rex kÊszÃŧlÊkÊre. +\n +\nKÊrjÃŧk, győződjÃļn meg rÃŗla, hogy telepíti a .fw fÃĄjlt, a .res fÃĄjlt, vÊgÃŧl pedig a .gps fÃĄjlt. Az Ãŗra a .fw fÃĄjl telepítÊse utÃĄn Ãējraindul. +\n +\nMegjegyzÊs: Nem kell telepítenie a .res Ês .gps fÃĄjlokat, ha azok pontosan megegyeznek a korÃĄbban telepítettel. +\n +\nFOLYTATÁS CSAK SAJÁT FELELŐSSÉGRE! + Ön telepíteni kívÃĄnja a %s firmware-t az Mi Band 4 kÊszÃŧlÊkÊre. +\n +\nKÊrjÃŧk, győződjÃļn meg rÃŗla, hogy előszÃļr a .fw fÃĄjlt telepítse, utÃĄna pedig a .res fÃĄjlt. A karkÃļtő a .fw fÃĄjl telepítÊse utÃĄn Ãējraindul. +\n +\nMegjegyzÊs: Nem kell telepítenie a .res Ês .gps fÃĄjlokat, ha azok pontosan megegyeznek a korÃĄbban telepítettel. +\n +\nFOLYTATÁS CSAK SAJÁT FELELŐSSÉGRE! + Ön telepíteni kívÃĄnja a %s firmware-t a Xiaomi Smart Band 7 kÊszÃŧlÊkÊre. +\n +\nA .zip fÃĄjl telepítÊse utÃĄn a karkÃļtője Ãējraindul. +\n +\nFOLYTATÁS CSAK SAJÁT FELELŐSSÉGRE! + Ön telepíteni kívÃĄnja a %s firmware-t az Amazfit GTS 4 Mini kÊszÃŧlÊkÊre. +\n +\nA .zip fÃĄjl telepítÊse utÃĄn a karkÃļtője Ãējraindul. +\n +\nFOLYTATÁS CSAK SAJÁT FELELŐSSÉGRE! + Ön telepíteni kívÃĄnja a %s firmware-t az Amazfit GTR 3 kÊszÃŧlÊkÊre. +\n +\nA .zip fÃĄjl telepítÊse utÃĄn a karkÃļtője Ãējraindul. +\n +\nFOLYTATÁS CSAK SAJÁT FELELŐSSÉGRE! + Ön telepíteni kívÃĄnja a %s firmware-t az Amazfit Band 7 kÊszÃŧlÊkÊre. +\n +\nA .zip fÃĄjl telepítÊse utÃĄn a karkÃļtője Ãējraindul. +\n +\nFOLYTATÁS CSAK SAJÁT FELELŐSSÉGRE! + Ön telepíteni kívÃĄnja a %s firmware-t az Amazfit Cheetah Pro kÊszÃŧlÊkÊre. +\n +\nA .zip fÃĄjl telepítÊse utÃĄn a karkÃļtője Ãējraindul. +\n +\nFOLYTATÁS CSAK SAJÁT FELELŐSSÉGRE! + A %s firmware-t kÊszÃŧl telepíteni a %s kÊszÃŧlÊkre. +\n +\nA .zip fÃĄjl telepítÊse utÃĄn az ÃŗrÃĄja Ãējraindul. +\n +\nFOLYTATÁS CSAK SAJÁT FELELŐSSÉGRE! \ No newline at end of file diff --git a/app/src/main/res/values-ia/strings.xml b/app/src/main/res/values-ia/strings.xml new file mode 100644 index 000000000..a6b3daec9 --- /dev/null +++ b/app/src/main/res/values-ia/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 0bbc73d5e..d762c02c9 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -2405,4 +2405,50 @@ Simboli Comuni Viola Blu + L\'icona Impostazioni è sempre visualizzata alla fine + Struttura menu: %s + Calorie + Vibrazione Intelligente + Nascondi solo il corpo + Consenti a Wena di chiedere periodicamente a Gadgetbridge di scaricare i dati di attività dal dispositivo. + Media + Struttura menu JSON invalida + Contapassi + Mi Band HRX + Recupero statistiche + Ora Corrente + Sincronizzazione Dati Attività in Background + Debole + Icone Schermata Iniziale + Bohemic Smart Bracelet + Cielo stellato + Amazfit Balance + Meteo Nella Barra Di Stato + Centigradi + Energia Corporea + Accendi il display quando guardi il polso + Reimposta struttura menu + Doppia Pressione + Amazfit Falcon + Amazfit Active Edge + Recupero dati temperatura + Mostra le condizioni meteo attuali nell\'angolo in alto a sinistra della schermata iniziale + Sony Wena 3 + Icone Menu + Media ossigeno nel sangue + Fahrenheit + Intensità Vibrazione + Pressione Prolungata + Pagamento + Impostazioni Sveglia + Struttura menu rimossa + Aggiungi rettangoli arrotondati intorno alle icone della schermata iniziale + Ricompila il quadrante dell\'orologio per il menu personalizzato + Amazfit Active + Struttura menu JSON impostata in GB + Forte + Schermta Attività + Impostazioni Notifiche + Sony WF-1000XM5 + Femometer Vinca II \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 83523144f..f6669efba 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -835,4 +835,5 @@ 青 ã‚ĸクテã‚Ŗビテã‚Ŗį”ģéĸ 通įŸĨč¨­åŽš + Bangle.js Gadgetbridge(Nightly) \ No newline at end of file diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index e2eccdf8e..0ad3b7b5c 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -304,7 +304,7 @@ ė œ 3ėž ė•ąėœŧ로 ëŗ´ë‚´ëŠ” 메ė„¸ė§€ė— ė–¸ė œë‚˜ 바로 ACKė„ ëŗ´ëƒ…니다 단ėœ„ ė‹œę°„ 형ė‹ - ė—°ę˛° + ė—°ę˛°â€Ļ ė§€ę¸ˆ 당ė‹ ė˜ Amazfit Corė— %s 펌ė›¨ė–´ëĨŧ ė„¤ėš˜í•˜ë ¤ęŗ  합니다. \n \n.fw 파ėŧė„ ė„¤ėš˜í•˜ęŗ  .res 파ėŧė„ ė„¤ėš˜í•œ 후 ėĩœėĸ…ė ėœŧ로 .gps 파ėŧė„ ė„¤ėš˜í•˜ė„¸ėš”. .fw 파ėŧė„ ė„¤ėš˜í•œ 후 기기가 ėžŦė‹œėž‘ 됩니다. @@ -611,4 +611,43 @@ ė ˆė „ ëĒ¨ë“œëŠ”ėŖŧ기ė ė¸ ė‹Ŧ박ėˆ˜ ėžë™ ė¸Ąė •ė„ 해ė œí•˜ė—Ŧ ėž‘ė—… ė‹œę°„ė„ 늘ëĻŊ니다. ëļ„ 단ėœ„ė˜ ėŠ¤ë§ˆíŠ¸ ė•ŒëžŒ 간격 ėŠ¤ë§ˆíŠ¸ ė•ŒëžŒ 간격ė€ ė„¤ėš˜ëœ ė•ŒëžŒ ė´ė „ė˜ 간격ėž…니다. ė´ 간격ė—ė„œ ėžĨėš˜ëŠ” ė‚ŦėšŠėžëĨŧ ꚨėš°ę¸° ėœ„í•´ 가ėžĨ 가ë˛ŧėš´ ėˆ˜ëŠ´ 단ęŗ„ëĨŧ 감ė§€í•˜ë ¤ęŗ í•Šë‹ˆë‹¤. + ė „ė› 끄기 + ėŧ + 2ėŖŧ + ė •í™•ë„ + Gadgetbridgeė— 대하ė—Ŧ + Bangle.js Gadgetbridge + Bangle.js Gadgetbridge + Bangle.js Gadgetbridgeė— 대하ė—Ŧ + ė „ė› 끄기 + 기기ė˜ ė „ė›ė„ 끄ęŗ  ė‹ļė€ 것ė´ 확ė‹¤í•œę°€ėš”? + 배터ëĻŦ 레벨 + ėŖŧ + ëŗ„ëĒ… ė„¤ė • + ėˆ˜ëŠ´ + ęą°ëĻŦ + 활ė„ą 깸ėŒ + ė‹Ŧ박ėˆ˜ ė¸Ąė • ė–ģ기 + 배터ëĻŦ ė •ëŗ´ + ė ˆė „ + ėģ¤ėŠ¤í…€ + ėˆ¨ę¸°ę¸° + 활동 ëĒŠëĄ + ëŗ€ę˛Ŋ 기록 + 균형 + ė €ė „ë Ĩ GPS + GPS + GPS + BDS + GPS + GNOLASS + GPS + GALILEO + ėˆ˜ëŠ´ 기간 표ė‹œ + ęą°ëĻŦ는 깸ėŒęŗŧ ëŗ´í­ė—ė„œ ęŗ„ė‚°ëŠë‹ˆë‹¤ (ė„¤ė •ė—ė„œ ëŗ€ę˛Ŋ 가ëŠĨ) + ė „ė˛´ 깸ėŒ ėˆ˜ 표ė‹œ + ë‹Ŧ + 3ë‹Ŧ + 6ë‹Ŧ + 년 + ė‹œėž‘ + 활ė„ą ė‹œę°„ + 활동 \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 4fcf9544d..ebc35458d 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -50,7 +50,7 @@ Als u nog steeds wilt doorgaan en uw apparaat blijft goed functioneren, vertel dan alstublieft de ontwikkelaars van Gadgetbridge om de %s firmware-versie op de goedgekeurde lijst te zetten. Instellingen Algemene instellingen - Verbind met Gadgetbridge-apparaat wanneer Bluetooth wordt ingeschakeld + Verbind met Gadgetbridge-apparaten wanneer Bluetooth wordt ingeschakeld Start automatisch Verbind automatisch opnieuw Gewenste audiospeler @@ -141,7 +141,7 @@ \nOpmerking: U hoeft de .res niet te installeren als deze exact dezelfde is als degene die ervoor al was geïnstalleerd. \n \nGA VERDER OP EIGEN RISICO! - Activeer links/rechts vegen in grafiek schermen + Activeer links/rechts vegen in grafiekschermen Weer Weerlocatie (voor LinageOS-weeraanbieder) Verwijder automatisch verworpen meldingen @@ -580,7 +580,7 @@ Huidige / Max hartslag: %1$d / %2$d Nacht modus Verlaag \'s nachts automatisch de scherm helderheid - Grafiek instellingen + Grafiekinstellingen Maximum hartslag Minimum hartslag OK @@ -673,7 +673,7 @@ Anti-verlies waarschuwing elke 15 minuten elke 45 minuten - Dagelijks doel: calorieÃĢn verbrandt + Dagelijks doel: calorieÃĢn verbrand Dagelijks doel: afstand in meters Dagelijks doel: tijd actief in minuten Mi Scale 2 @@ -712,7 +712,7 @@ Ondergrens Bovengrens Gemiddelde: %1$s - Grafiek instellingen + Grafiekinstellingen Toon gemiddelden in de grafieken Grafiekbereik Grafiekbereik ingesteld op 1 maand @@ -2268,11 +2268,11 @@ OK Titel %d min - PAI totaal + Totaal PAI per week +%d PAI per maand - Dag PAI-toename + Dag-toename Catima-pakketnaam Gearchiveerde kaarten synchroniseren Alleen kaarten met ster synchroniseren @@ -2530,4 +2530,277 @@ Thermometer Femometer Vinca II Navigatie-app niet geïnstalleerd op horloge + Amazfit Active Edge + Zuurstofsaturatie + Amazfit Active + Navigatie-apps + Navigatie-instellingen + Google Maps + Plaats app-naam vooraan meldingen + Selecteer met welke versie van OsmAnd verbonden moet worden + App-naam in melding + OsmAnd-pakketnaam + OsmAnd(+) + Overige meldingen + Kalendermeldingen + Tril/piep voor binnenkomende telefoongesprekken + Xiaomi Smart Band 8 + Hardlopen + Planning slaapmodus + Melding voor telefoongesprekken + Bedtijd + Melding voor nieuwe e-mail + Tril/piep bij e-mailmeldingen + Focus + Tril/piep voor SMS-meldingen + Toon een preview van het bericht in de titel + Redmi Watch 3 Active + Tril/piep voor kalendermeldingen + Serienummer + Deens + Wektijd + Statistieken + Tril/piep voor meldingen in de categorie Overig + Xiaomi Watch Lite + Laat een preview van het bericht zien in de titel van een melding voor zover toegestaan door het apparaat + Stuur een melding en start slaapmodus op de ingestelde bedtijd. Op de ingestelde wektijd klinkt een alarmsignaal. + Melding voor SMS + Meldingen + Xiaomi Smart Band 7 Pro + Xiaomi Watch S1 Active + Mi Watch Color Sport + Pixoo + Stuur app-meldingen naar het apparaat + Stuur meldingen + Niet storen - Aan + Stiltestand telefoon + Draagwijze + Pebble (schoengesp) + Normaal / Trillen + Tweede doel + Krijg een melding wanneer je vitaliteitsscore 30, 60 of 100 bereikt in de afgelopen 7 dagen + Vitaliteitsscore + Niet storen - Alleen alarmen + Trillen / Stil + Krijg een melding wanneer je het dagelijks maximum aantal vitaliteitspunten hebt bereikt + Niet storen - Alleen belangrijk + Niet ingesteld + Tijd staand + Tijd actief + Band (armband) + Ketting (om de nek) + Dagelijkse voortgang + 7-daagse voortgang + Niet storen - Uit + Normaal / Stil + Trail run + Widget-layout + Het apparaat heeft geen vrije plekken voor widget-schermen (totaal aantal plekken: %1$s) + Onbekende training - %s + Omhoog + Weet u zeker dat u \'%1$s\' wilt verwijderen? + Klap in handen voor scherm aan" + Wandeltocht + 2 widgets + Verwijder widget-scherm + Klap nog een keer voor scherm uit" + 1 boven, 2 onder + ColaCao 2023 + 2 boven, 2 onder + Er moeten minimaal %1$s schermen zijn + Widget + Redmi Smart Band 2 + Widget-scherm + Omlaag + 1 widget + Widget-subtype + Scherm %s + Selecteer alle widgets + 2 boven, 1 onder + ColaCao 2021 + Het scherm zal uitgaan nadat de microfoon een tijd geen geluid heeft opgevangen + Worstelen + Mijia thermo- en hygrometer 2 + Navigatie-instructies + Configureer gedrag van navigatie-app op horloge + Mag de navigatie-app automatisch naar de voorgrond komen als er een navigatie-instructie ontvangen wordt? + Tril bij nieuwe instructie + Naar voorgrond + Apparaatnaam + Moet het horloge trillen bij elke nieuwe of veranderde navigatie-instructie (alleen als de app op de voorgrond is)? + Redmi Watch 2 Lite + Connectie status + Redmi Smart Band Pro + Nothing Ear (2) + Alleen wanneer scherm-aan-bij-oppakken is ingeschakeld + Niet storen wanneer het apparaat niet gedragen wordt + Handmatig + Hele dag + Honor Band 3 + Honor Band 4 + Honor Band 5 + Honor Band 6 + Honor Band 7 + Huawei Band (AW70) + Huawei Band 6 + Huawei Band 7 + Huawei Band 8 + Huawei Watch GT + Huawei Band 4 (Pro) + Huawei Watch GT 2 (Pro) + Huawei Watch GT 2e + Huawei Talk Band B6 + Huawei Watch GT 3 (Pro) + Lichte actieve geluidsonderdrukking + Transparantie + Verwijder het vinkje niet bij slim wakker worden. + HUAWEI TruSleep â„ĸ + Verbeterde slaapmonitoring + Telefoontjes accepteren inschakelen + Het op het apparaat accepteren van binnenkomende telefoongespreken inschakelen + Telefoontjes weigeren inschakelen + Het op het apparaat weigeren van binnenkomende telefoongespreken inschakelen + Vind mijn telefoon uitschakelen wanneer Niet Storen actief is + Automatische hartslagmeting inschakelen + Automatische bloedzuurstofmeting inschakelen + Melding op het apparaat bij verbroken Bluetooth-verbinding. + Nothing Ear (Stick) + Werkmodus + Plaats geen vinkje bij slim wakker worden. + Monitor uw slaapkwaliteit en adempatroon in realtime. +\nAnalyseer uw slaappatronen en diagnosticeer 6 types slaapproblemen. + Opties forceren + Sommige apparaten geven onterecht aan sommige opties niet te ondersteunen. Deze instellingen gebruikt worden om ze alsnog in te schakelen. +\nGEBRUIK OP EIGEN RISICO +\nLees de documentatie + Forceer slim alarm + Forceer ondersteuning van slimme alarmen +\nGEBRUIK OP EIGEN RISICO + Maak alleen opnieuw verbinding met verbonden apparaten + Maak alleen opnieuw verbinding met verbonden apparaten, in plaats van met alle apparaten + Het batterijniveau van het apparaat is te laag + Forceer draaglocatie + Forceer ondersteuning voor Niet Storen + Negeer startstatus wakkerworden + Kan helpen voor correcte slaapdetectie. Direct zichtbaar in het dagelijkse activiteitenoverzicht. + Negeer eindstatus wakkerworden + Kan helpen voor correcte slaapdetectie. Direct zichtbaar in het dagelijkse activiteitenoverzicht. + Forceer ondersteuning voor draaglocatie +\nGEBRUIK OP EIGEN RISICO + Forceer ondersteuning voor Niet Storen +\nGEBRUIK OP EIGEN RISICO + Trainingsgegevens opnieuw analyseren + Stuur een foutopsporingsverzoek naar het Huawei-apparaat + Foutopsporingsverzoek + Dit zal alleen iets doen na bepaalde updates + Zoemerintensiteit + Afstand zonder hoogteverschil + Versie 2 + Versie 3 + Protocolversie + Het wachtwoord moet uit 4 cijfers bestaan + Xiaomi Smart Band 8 Pro + Mijia MHO-C303 + Versie 1 + Xiaomi Watch S1 + Xiaomi Watch S3 + Xiaomi Watch S1 Pro + Wijzerplaat uploadenâ€Ļ + Wijzerplaat uploaden + Wijzerplaat-installatie gelukt + Wijzerplaat-installatie mislukt + Forceer verbindingstype + U kunt proberen het type verbinding te forceren indien het Gadgetbridge niet lukt om verbinding te maken + Automatisch + Bluetooth LE + Bluetooth (klassiek) + Activiteitsinformatie + 155 bpm + 165 bpm + 175 bpm + 185 bpm + 195 bpm + 205 bpm + Schakel drinkherinneringen tijdelijk uit + Binnen hardlopen + Bergwandelen + Veldlopen + Karate + Fitnessoefeningen + Crossfit + Jazz-dansen + Latin-dansen + Jetski + Skaten + Ballet + Overige dansen + Sneeuwsporten + Redmi Watch 2 + CMF Watch Pro + Meldingsdrempel maximale hartslag + Crosstrainer + Vrije training + Roeimachine + Dynamisch fietsen + Traploop-apparaat + Functionele training + Fysieke training + Taekwondo + Schermen + Kendo + Rekstok + Brug met gelijke leggers + Afkoelen + Crosstraining + Sit-ups + Fitness-gamen + Aerobics + Rollen + Rekken + Atletiek + Push-ups + Battle rope + Smith machine + Pull-ups + Planken + Speerwerpen + Verspringen + Hoogspringen + Trampoline + Dumbbell + Buikdansen + Rolschaatsen + Vechtsporten + Tai chi + Hoelahoep + Discuswerpen + Darten + Boogschieten + Paardrijden + Kiten + Schommelen + Traplopen + Vissen + Handfietsen + Geest en lichaam + Kabaddi + Karten + Biljard + Badminton + Softbal + Trefbal + Australisch football + Pickleball + Lacrosse + Shot + Zeilen + IJshockey + Curling + Langlaufen + Skateboarden + Rotsklimmen + Jagen + Buiten wandelen + Sony WI-SP600N + Honor MagicWatch 2 \ No newline at end of file diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index a630980b4..5804dd6b2 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -39,13 +39,13 @@ Ustawienia Ustawienia ogÃŗlne - Połącz z urządzeniem Gadgetbridge, gdy Bluetooth jest włączony + Połącz urządzenia z Gadgetbridge, gdy Bluetooth jest włączony Automatycznie ponÃŗw połączenie Preferowany odtwarzacz muzyki Domyślny Data i godzina Synchronizuj czas - Synchronizuj czas urządzenia Gadgetbridge podczas łączenia lub gdy czas albo strefa czasowa zmienią się na urządzeniu Android + Synchronizuj czas urządzenia podczas łączenia lub gdy czas albo strefa czasowa zmienią się na urządzeniu Android Motyw Jasny Ciemny @@ -105,14 +105,14 @@ Kliknij połączone urządzenie, aby uruchomić menadÅŧer aplikacji Dotknij urządzenie aby połączyć Nie moÅŧna połączyć. Nieprawidłowy adres Bluetooth\? - Gadgetbridge działa + Gadgetbridge jest uruchomiony Instalowanie binarki %1$d/%2$d Instalacja nie powiodła się Zainstalowano - PRÓBUJESZ ZAINSTALOWAĆ FIRMWARE, POSTĘPUJESZ NA WŁASNĄ ODPOWIEDZIALNOŚĆ . -\n -\n -\n Ten firmware jest przeznaczony dla wersji HW: %s + PRÓBUJESZ ZAINSTALOWAĆ OPROGRAMOWANIE UKŁADOWE, KONTYNUUJ NA WŁASNE RYZYKO. +\n +\n +\n Te oprogramowanie układowe jest przeznaczone dla wersji HW: %s Zamierzasz zainstalować poniÅŧszą aplikację: \n \n%1$s @@ -220,7 +220,7 @@ KrokÃŗw dziś, cel: %1$s Nie przesyłaj danych aktywności ACK Jeśli transmisja danych nie zostanie potwierdzona przez opaskę, dane dotyczące aktywności nie zostaną wyczyszczone. Przydatne, jeśli GB jest uÅŧywany razem z innymi aplikacjami. - Dane aktywności będą zachowane na urządzeniu nawet po synchronizacji. Jest to przydatne, gdy Gadgetbridge jest uÅŧywany razem z innymi aplikacjami. + Dane aktywności będą zachowane na urządzeniu nawet po synchronizacji. Jest to przydatne, gdy Urządzenia na ciele jest uÅŧywany razem z innymi aplikacjami. MoÅŧe pomÃŗc na urządzeniach gdzie aktualizacja kończy się błędem. Historia krokÃŗw Aktualnie krokÃŗw/min @@ -498,7 +498,7 @@ Nie zmierzono Aktywność Urządzenie nie noszone - Chodzenie + Spacer Nieznana aktywność Aktywności sportowe Jazda na rowerze @@ -519,7 +519,7 @@ Godziny: Sekundy: Kalibruj - Prosimy pamiętać, Åŧe logi Gadgetbridge mogą zawierać wiele informacji osobistych, w tym dane dotyczące zdrowia, unikalne identyfikatory (takie jak adresy MAC urządzeń), preferencje muzyczne itd. RozwaÅŧ edycję logÃŗw i usunięcie takich informacji zanim wyślesz je przy zgłaszaniu problemu. + Prosimy pamiętać, Åŧe logi Gadgetbridge mogą zawierać wiele informacji osobistych, w tym dane dotyczące zdrowia, unikalne identyfikatory (takie jak adresy MAC urządzeń), preferencje muzyczne itd. RozwaÅŧ edycję logÃŗw i usunięcie takich informacji, zanim wyślesz je przy zgłaszaniu problemu. OstrzeÅŧenie! Brak danych Kolor LED @@ -541,7 +541,7 @@ Koniec Odblokowanie ekranu opaski Przesuń w gÃŗrę, aby odblokować ekran opaski - Norwegian BokmÃĨl + Norweski BokmÃĨl Przeniesienie danych z okresu %1$s Ustawienie alarmu na %1$02d:%2$02d Błąd tworzenia folderu na pliki log: %1$s @@ -682,7 +682,7 @@ Sen w miesiącu Mijia Smart Clock NFC - Zezwala innym aplikacjom na dostęp do danych HR w czasie rzeczywistym, gdy Gadgetbridge jest połączony + UmoÅŧliwia innym aplikacjom dostęp do danych HR w czasie rzeczywistym, gdy Gadgetbridge jest podłączony UÅŧyj niestandardowej czcionki Włącz tę opcję, jeśli Twoje urządzenie ma niestandardowy firmware czcionek do obsługi emoji Automatyczny eksport @@ -845,7 +845,7 @@ Wymuś schemat kolorÃŗw czarno na białym UÅŧyteczne, jeśli twÃŗj zegarek ma ciemne wskazÃŗwki Wysoki priorytet - PokaÅŧ specyficzną dla urządzenia ikonę powiadomienia zamiast ikony Gadgetbridge, gdy połączono + PokaÅŧ specyficzną dla urządzenia ikonę powiadomienia kiedy zostanie połączone zamiast ikony Gadgetbridge Trening Stoper Siła wibracji @@ -903,7 +903,7 @@ Ostatnie powiadomienie Ustaw własną nazwę (alias) WspÃŗłtwÃŗrcy - O Gadgetbridge + Informacje o Gadgetbridge Amazfit T-Rex Styl grzbietowy Styl dowolny @@ -1021,7 +1021,7 @@ Linki Dziękujemy wszystkim nie notowanym na liście wspÃŗłtwÃŗrcom za udostępnienie kodu, tłumaczenia, wsparcie, pomysły, motywację, zgłoszenia błędÃŗw, pieniądzeâ€Ļ ✊ ZespÃŗł głÃŗwny (w kolejności od pierwszego wkładu w kod aplikacji) - Niewymagający chmury wolny zamiennik dla zamkniętoÅērÃŗdłowych aplikacji dostarczanych przez producentÃŗw gadÅŧetÃŗw na system Android. + Niewymagający chmury wolny zamiennik dla zamknięto ÅērÃŗdłowych aplikacji dostarczanych przez producentÃŗw gadÅŧetÃŗw na system Android. Edytuj etykietę Ping-pong Krykiet @@ -1178,7 +1178,7 @@ Amazfit GTS 2e Informacje o baterii Amazfit X - Właśnie zamierzasz zainstalować oprogramowanie układowe %s na swoim Amazfit X. + Właśnie zamierzasz zainstalować oprogramowanie układowe %s na Twoim Amazfit X. \n \nProszę upewnić się, Åŧe zainstalowany został plik .fw, a następnie plik .res. Po zainstalowaniu pliku .fw Twoja opaska zostanie ponownie uruchomiona. \n @@ -1208,7 +1208,7 @@ NiektÃŗre funkcje są wyłączone, poniewaÅŧ oprogramowanie zegarka jest zbyt nowe Skonfiguruj, kiedy urządzenie będzie emitować sygnał dÅēwiękowy DÅēwięki - Właśnie zamierzasz zainstalować oprogramowanie układowe %s na swoim Amazfit Neo. + Właśnie zamierzasz zainstalować oprogramowanie układowe %s na Twoim Amazfit Neo \n \nPo zainstalowaniu pliku .fw Twoja opaska zostanie ponownie uruchomiona. \n @@ -1611,19 +1611,19 @@ Tekst jako bitmapy ZezwÃŗl na dostęp do internetu ZezwÃŗl aplikacjom na tym urządzeniu na dostęp do Internetu - 100 uderzeń + 100 uderzeń/min Usuń \'%1$s\' Strefa czasowa - 105 uderzeń - 110 uderzeń + 105 uderzeń/min + 110 uderzeń/min 112 bpm - 120 uderzeń - 125 uderzeń - 130 uderzeń - 135 uderzeń - 140 uderzeń - 145 uderzeń - 150 uderzeń + 120 uderzeń/min + 125 uderzeń/min + 130 uderzeń/min + 135 uderzeń/min + 140 uderzeń/min + 145 uderzeń/min + 150 uderzeń/min Spotify 5 sekund 10 sekund @@ -1649,7 +1649,7 @@ Kalendarz Zdrowie Czas - 40 uderzeń + 40 uderzeń/min 45 uderzeń/min 50 uderzeń/min 80% @@ -1696,11 +1696,11 @@ Automatyczne monitorowanie poziomu tlenu we krwi przez cały dzień PrÃŗg alarmowy SPO2 Monitorowanie jakości oddechu podczas snu - Zamierzasz zainstalować oprogramowanie %s na swoim Xiaomi Smart Band 7. + Zamierzasz zainstalować oprogramowanie układowe %s na swoim Xiaomi Smart Band 7. \n \nTwoja opaska uruchomi się ponownie po zainstalowaniu pliku .zip. \n -\nROBISZ TO NA WŁASNĄ ODPOWIEDZIALNOŚĆ! +\nKONTYNUUJ NA WŁASNE RYZYKO! Proszę podłączyć TYLKO JEDNO urządzenie, do ktÃŗrego chcesz wysłać plik. Połączenie Wyświetlanie @@ -1719,16 +1719,16 @@ Aplikacja towarzysząca dla Bangle.js, zbudowana na bazie projektu Gadgetbridge, z dodanym dostępem do Internetu. \n \nZe względu na politykę Sklepu Google Play, nie moÅŧemy umieścić linku do darowizny w samej aplikacji, ale jeśli Ci się ona podoba, rozwaÅŧ przekazanie darowizny poprzez stronę głÃŗwną Gadgetbridge poniÅŧej. - Gadgetbridge (Wczesna) - Gadgetbridge Wczesna - O Gadgetbridge Nightly - Niewymagający chmury wolny zamiennik dla zamkniętoÅērÃŗdłowych aplikacji dostarczanych przez producentÃŗw gadÅŧetÃŗw na system Android. Wydania Nightly Gadgetbridge. Nie moÅŧna go zainstalować, jeśli masz juÅŧ zainstalowaną aplikację Gadgetbridge lub Pebble, ze względu na konflikt w dostawcy Pebble. - Gadgetbridge (Nightly) działa - Gadgetbridge (Nightly, bez dostawcy Pebble) - Gadgetbridge Nightly Bez Pebble - O Gadgetbridge Nightly Bez Pebble - Niewymagający chmury wolny zamiennik dla zamkniętoÅērÃŗdłowych aplikacji dostarczanych przez producentÃŗw gadÅŧetÃŗw na system Android. Wydania Nightly Gadgetbridge. W tej wersji zmieniono nazwę dostawcy Pebble, aby zapobiec konfliktom, więc niektÃŗre integracje związane z Pebble nie będą działać, ale moÅŧna ją zainstalować obok istniejącej instalacji Gadgetbridge. - Gadgetbridge (Nightly Bez Pebble) działa + Gadgetbridge (Nightly) + Gadgetbridge Nightly + Informacje o Gadgetbridge Nightly + Niewymagający chmury wolny zamiennik dla zamknięto ÅērÃŗdłowych aplikacji dostarczanych przez producentÃŗw gadÅŧetÃŗw na system Android. Wczesne wydanie Gadgetbridge. Nie moÅŧna go zainstalować, jeśli masz juÅŧ zainstalowaną aplikację Urządzenia na ciele lub Pebble, ze względu na konflikt w dostawcy Pebble. + Gadgetbridge (Nightly) jest uruchomiony + Gadgetbridge (Nightly, No Pebble provider) + Gadgetbridge Nightly NoPebble + Informacje o Gadgetbridge Nightly NoPebble + Niewymagający chmury wolny zamiennik dla zamknięto ÅērÃŗdłowych aplikacji dostarczanych przez producentÃŗw gadÅŧetÃŗw na system Android. Wczesne wydanie Gadgetbridge. W tej wersji zmieniono nazwę dostawcy Pebble, aby zapobiec konfliktom, więc niektÃŗre integracje związane z Pebble nie będą działać, ale moÅŧna ją zainstalować obok istniejącej instalacji Gadgetbridge. + Gadgetbridge (Nightly NoPebble) jest uruchomiona Jeśli słowo nie moÅŧe być wygenerowane za pomocą czcionki zegarka, wygeneruj je do bitmapy w Gadgetbridge i wyświetl ją na zegarku Ta funkcja ma potencjał, aby pozostawić z Twojego urządzenia cegłę. To powiedziawszy, taka sytuacja nigdy nie zdarzyła się Åŧadnemu z programistÃŗw poprzez flashowanie, ale pamiętaj, Åŧe robisz to na własne ryzyko. Pobieranie pliku z oprogramowaniem sprzętowym/aplikacją @@ -1752,13 +1752,13 @@ PoniewaÅŧ nie moÅŧemy rozpowszechniać plikÃŗw oprogramowania sprzętowego, będziesz musiał zdobyć je samodzielnie. Oznacza to, Åŧe będziesz musiał szukać plikÃŗw w plikach apk, online, na forach, na Amazfitwatchfaces (dla urządzeń Miband/Amazfit) i tak dalej. Proszę podłączyć PRZYNAJMNIEJ JEDNO urządzenie, do ktÃŗrego chcesz wysłać plik. Inicjalizacja logowania plikÃŗw nie powiodła się, zapisywanie plikÃŗw dziennika jest obecnie niedostępne. Uruchom ponownie aplikację, aby sprÃŗbować ponownie zainicjować pliki dziennika. - Bangle.js Gadgetbridge (Wczesna) - Bangle.js Gadgetbridge (Wczesna) + Bangle.js Gadgetbridge (Nightly) + Bangle.js Gadgetbridge (Nightly) O Bangle.js Gadgetbridge (Nightly) Aplikacja towarzysząca dla Bangle.js, zbudowana na bazie projektu Gadgetbridge, z dodanym dostępem do Internetu. \n \nZe względu na politykę Sklepu Google Play, nie moÅŧemy umieścić linku do darowizny w samej aplikacji, ale jeśli Ci się ona podoba, rozwaÅŧ przekazanie darowizny poprzez stronę głÃŗwną Gadgetbridge poniÅŧej. - Bangle.js (Nightly) działa + Bangle.js jest uruchomiony (Nightly) OpÃŗÅēnienie przed wysłaniem powiadomień o połączeniach przychodzących do urządzenia, w sekundach. Czy na pewno chcesz usunąć zegar światowy\? Alarm tętna (eksperymentalny) @@ -1773,11 +1773,11 @@ PrÃŗg alarmowy wysokiego tętna Monitorowanie poziomu stresu podczas odpoczynku Ustaw preferencje - PokaÅŧ detale + PokaÅŧ szczegÃŗły Połączono: %d/%d Eliptyczna Błąd przy usuwaniu urządzenia: %s - GPS Gadgetbridge + Gadgetbridge GPS Nieznane (%s) Czujnik binarny Wysyłanie lokalizacji GPS do %1$d urządzeń @@ -1928,11 +1928,11 @@ Klasyczne Turbo Speed Odrzuć - Właśnie zamierzasz zainstalować oprogramowanie układowe %s na Amazfit GTR3. -\n -\nUrządzenie uruchomi się ponownie po instalacji pliku .zip. -\n -\nWykonujesz to na własne ryzyko! + Właśnie zamierzasz zainstalować oprogramowanie układowe %s na Twoim Amazfit GTR3. +\n +\nUrządzenie uruchomi się ponownie po instalacji pliku .zip. +\n +\nKONTYNUUJ NA WŁASNE RYZYKO! PozwÃŗl aplikacjom firm trzecich na zmianę ustawień Taniec Ćwiczenia w domu @@ -1979,7 +1979,7 @@ \n \nOpaska uruchomi się ponownie po instalacji *.zip. \n -\nKontynuujesz na własne ryzyko! +\nKONTYNUUJ NA WŁASNE RYZYKO! Lista ignorowanych powiadomień ze stron społecznościowych Przetwarzaj powiadomienia multimedialne przed listą aplikacji. Jeśli ta preferencja nie jest zaznaczona, aplikacje multimedialne muszą być dozwolone na liście aplikacji, aby elementy sterujące multimediami działały na urządzeniu. Automatyczne wykrywanie kategorii ćwiczeń @@ -2079,7 +2079,7 @@ \n \nOpaska uruchomi się ponownie po instalacji *.zip. \n -\nKontynuujesz na własne ryzyko! +\nKONTYNUUJ NA WŁASNE RYZYKO! DÅēwięki i wibracje Interfejs API intencji Tekst na mowę @@ -2105,12 +2105,12 @@ \n \nOpaska uruchomi się ponownie po instalacji *.zip. \n -\nKontynuujesz na własne ryzyko! +\nKONTYNUUJ NA WŁASNE RYZYKO! Zamierzasz zainstalować oprogramowanie układowe %s na Twoim Amazfit GTR 4. \n \nOpaska uruchomi się ponownie po instalacji *.zip. \n -\nKontynuujesz na własne ryzyko! +\nKONTYNUUJ NA WŁASNE RYZYKO! Czas powiadamiania o wygasaniu AGPS Kierunek ćwiczeń UmoÅŧliwi to dostęp do wszystkich dostępnych ustawień, nawet jeśli nie są obsługiwane przez urządzenie. MoÅŧe to spowodować niestabilność i awarie urządzenia. @@ -2121,7 +2121,7 @@ \n \nOpaska uruchomi się ponownie po instalacji *.zip. \n -\nKontynuujesz na własne ryzyko! +\nKONTYNUUJ NA WŁASNE RYZYKO! Mieszanie GPS Wyszukiwanie satelit Wibracja koronki @@ -2129,7 +2129,7 @@ Przykryj, aby wyciszyć Automatyczny kierunek ćwiczeń Lista Do zrobienia - Właśnie zamierzasz zainstalować oprogramowanie układowe %s na Amazfit GTR 3 Pro. + Właśnie zamierzasz zainstalować oprogramowanie układowe %s na Twoim Amazfit GTR 3 Pro. \n \nUrządzenie uruchomi się ponownie po instalacji pliku .zip. \n @@ -2147,7 +2147,7 @@ Zreleksowany Ślad GPX Ustawienia Mi Band 1/2 - chorwacki + Chorwacki Zatrzymaj Casio GMW-B5000 Długie naciśnięcie @@ -2227,7 +2227,7 @@ UmoÅŧliwia zegarkowi wyzwalanie aparatu telefonu Ustawienia połączeń Bluetooth Parowanie połączeń Bluetooth - Właśnie zamierzasz zainstalować oprogramowanie układowe %s na swoim Amazfit Cheetah Pro + Właśnie zamierzasz zainstalować oprogramowanie układowe %s na Twoim Amazfit Cheetah Pro \n \nTwÃŗj zegarek zostanie ponownie uruchomiony po instalacji pliku .zip \n @@ -2235,7 +2235,7 @@ Pamięć podręczna poza zasięgiem Do określenia lokalizacji uÅŧywaj tylko dostawcy sieci. Zmniejsza to zuÅŧycie energii kosztem dokładności. Konieczne jest ponowne połączenie. Interwał aktualizacji danych GPS - Właśnie zamierzasz zainstalować oprogramowanie układowe %s na swoim %s. + Właśnie zamierzasz zainstalować oprogramowanie układowe %s na Twoim %s. \n \nTwÃŗj zegarek zostanie ponownie uruchomiony po instalacji pliki .zip. \n @@ -2259,7 +2259,7 @@ Ikona stanu Tytuł Pobieranie danych SpO2 - French (Canada) + Francuski (Kanada) PodwÃŗjne dotknięcie Blokada Qrio Zatrzymaj rejestrowanie dziennikÃŗw aplikacji zegarka @@ -2274,7 +2274,7 @@ Zainstaluj Catimę Do lewej Bez LED - Spanish (Spain) + Hiszpański (Hiszpania) Narciarstwo Udostępnianie pliku nie powiodło się. Kategorie porannych aktualizacji @@ -2289,7 +2289,7 @@ Ukryj tylko głÃŗwną treść PozwÃŗl urządzeniu Wena okresowo prosić Gadgetbridge o pobranie danych o aktywności z urządzenia Lista kategorii wyświetlana kaÅŧdego ranka - English (India) + Angielski (Indie) RÃŗwnowaga Suica Styl biznesowy Średnie @@ -2353,7 +2353,7 @@ Większy rozmiar czcionki Przesyłanie trasy gpx Parowanie z zegarkiem nie powiodło się - English (Australia) + Angielski (Australia) Energia ciała %1$s umoÅŧliwia wysyłanie wiadomości i innych danych z Androida na Twoje urządzenie. Aby to zrobić, wymagany jest dostęp do tych danych i bez niego moÅŧe nie działać poprawnie. \n @@ -2392,7 +2392,7 @@ Przesyłanie trasy Gpx nie powiodło się Uruchom serwer FTP na zegarku Spowoduje to pełną synchronizację wszystkich danych dotyczących aktywności z urządzenia. Ukończenie moÅŧe zająć kilka minut. - Dzienny wzrost PAI + Dzienny wzrost Usługa głosowa Akcja przycisku Czas wznowienia działania @@ -2412,7 +2412,7 @@ Kolor diody LED połączenia przychodzącego Pełna synchronizacja Synchronizowanie %d kart lojalnościowych z urządzeniem - French (France) + Francuski (Francja) Zatrzymaj hotspot Wi-Fi na zegarku Zezwalaj na polecenia debugowania Nie moÅŧna zainstalować pliku, urządzenie nie jest obsługiwane. @@ -2426,14 +2426,14 @@ Wybierz, czy urządzenie ma uÅŧywać skali Celsjusza czy Fahrenheita. Ikony Menu Dalej - Spanish (Mexico) + Hiszpański (Meksyk) Synchronizuj tylko karty oznaczone gwiazdką Lista zmian Skonfiguruj kontakty na zegarku Nie udało się otworzyć sklepu z aplikacjami, aby zainstalować Catimę Miniaturka Synchronizuj - English (United Kingdom) + Angielski (Wielka Brytania) PowtÃŗrzenie wibracji połączenia przychodzącego Zezwalaj na uruchamianie poleceń menu debugowania za pośrednictwem interfejsu Intent API Opcje synchronizacji @@ -2446,17 +2446,17 @@ Piłka ręczna Skala Fahrenheita Siła wibracji - English (United States) + Angielski (Stany Zjednoczone) Dynamiczne kolory nie są dostępne na Twoim urządzeniu, tylko Android 12+ obsługuje tę funkcję. Gadgetbridge uÅŧyje domyślnych kolorÃŗw Material 3. Wstecz Zatrzymaj serwer FTP na zegarku Więcejâ€Ļ - Spanish (United States) + Hiszpański (Stany Zjednoczone) Jeśli ta opcja jest wyłączona, nie będziesz otrzymywać powiadomień o połączeniach przychodzących na urządzeniu Wena Wiele danych Długie naciśnięcie Płatności - Latvian + Łotewski Strona stanu zamÃŗwień Pojedyncze dotknięcie Ustawienia alarmÃŗw @@ -2468,7 +2468,7 @@ Dodaje zaokrąglone prostokąty wokÃŗł ikon ekranu głÃŗwnego Przebuduj tarczę zegarka, aby uzyskać niestandardowe menu Withings Steel HR - Całkowite PAI + Całkowite Minimalistyczny Catima jest potrzebna do zarządzania kartami lojalnościowymi Kolor diody powiadomień @@ -2481,7 +2481,7 @@ Numer telefonu Parowanie z zegarkiem powiodło się UÅŧyj bogatego projektu - English (Canada) + Angielski (Kanada) Dzień zaczyna się o Aby skanowanie działało prawidłowo, naleÅŧy przyznać i włączyć dostęp do połączenia Bluetooth Karty skrÃŗtÃŗw widoczne po przesunięciu palcem w prawo na tarczy zegarka. To ustawienie nie ma wpływu na automatycznie generowane karty, gdy aplikacja jest uruchomiona. @@ -2539,4 +2539,250 @@ Termometr Femometer Vinca II Aplikacja do nawigacji nie jest zainstalowana na zegarku + Amazfit Active Edge + Średnia zawartość tlenu we krwi + Amazfit Active + Aplikacje nawigacyjne + Preferencje nawigacji + Mapy Google + Tytuł powiadomienia poprzedÅē nazwą aplikacji ÅērÃŗdłowej + SłuÅŧy do wyboru wersji OsmAnd, z ktÃŗrą chcesz się połączyć + Nazwa aplikacji w powiadomieniu + Nazwa pakietu OsmAnd + OsmAnd(+) + Alert dla pozostałych powiadomień + Alert powiadomień z kalendarza + Mi Watch Color Sport + Alert (wibracja/beep) dla połączeń przychodzących + Xiaomi Smart Band 8 + Bieganie + Harmonogram snu + Alert dla połączeń przychodzących + Pora snu + Alert powiadomień email + Alert (wibracja/beep) dla powiadomień email + Skupienie + Alert (wibracja/beep) dla powiadomień SMS (wiadomości tekstowych) + PokaÅŧ podgląd wiadomości w tytule + Redmi Watch 3 Active + Alert (wibracja/beep) dla powiadomień z kalendarza + Numer seryjny + Duński + Pobudka + Xiaomi Smart Band 7 Pro + Statystyki + Alert (wibracja/beep) dla pozostałych powiadomień + Xiaomi Watch Lite + PokaÅŧ podgląd wiadomości w tytule powiadomienia, jeÅŧeli udzielone jest odpowiednie uprawnienie + Wyślij przypomnienie i przed snem wejdÅē w tryb uśpienia. O zaplanowanej godzinie pobudki zabrzmi alarm. + Alert powiadomień SMS + Alerty + Xiaomi Watch S1 Active + Pixoo + Drugi punkt + Wysyłaj powiadomienia do urządzenia + Opaska (na nadgarstek) + Wysyłaj powiadomienia + Bieg szlakiem + Układ widÅŧetu + Urządzenie nie ma wolnych miejsc na ekrany widÅŧetÃŗw (łączna liczba miejsc: %1$s) + Nie przeszkadzać – Włączone + Tryb cichy telefonu + Nieznany trening - %s + Tryb noszenia + Pebble (klamra do butÃŗw) + Przesuń w gÃŗrę + Czy na pewno chcesz usunąć \'%1$s\'? + Klaśnij dłońmi, aby wybudzić ekran" + Normalny / Wibracje + Otrzymuj powiadomienie, gdy TwÃŗj wynik witalności osiągnie 30, 60 lub 100 w ciągu ostatnich 7 dni + Trekking + Wynik witalności + 2 widÅŧety + Usuń ekran widÅŧetu + Nie przeszkadzać — tylko alarmy + Ponownie klaśnięcie spowoduje wygaszenie ekranu" + 1 gÃŗra, 2 dÃŗł + ColaCao 2023 + 2 gÃŗra, 2 dÃŗł + Czujnik temperatury i wilgotności Mijia 2 + Musi być co najmniej %1$s ekranÃŗw + Wibracje / Cichy + WidÅŧet + Otrzymuj powiadomienie, gdy osiągniesz maksymalną liczbę punktÃŗw witalności na dany dzień + Redmi Smart Band 2 + Ekran widÅŧetu + Przesuń w dÃŗł + 1 widÅŧet + Nie przeszkadzać – tylko priorytetowe + Nie ustawiono + Podtyp widÅŧetu + Czas stania + Czas aktywności + Ekran %s + Wybierz wszystkie widÅŧety + 2 gÃŗra, 1 dÃŗł + ColaCao 2021 + Necklace (pasek na szyję) + Ekran wyłączy się, gdy mikrofon wykryje przez chwilę ciszę + Dzienny postęp + 7-dniowy postęp + Nie przeszkadzać – wyłączone + Normalny / Cichy + Zapasy + Instrukcje nawigacji + Konfiguracja zachowania aplikacji do nawigacji w zegarku + Określ, czy aplikacja nawigacyjna powinna automatycznie wyświetlać się na pierwszym planie po otrzymaniu instrukcji nawigacyjnych + Wibruj przy nowej instrukcji + PrzejdÅē do pracy na pierwszym planie + Nazwa urządzenia + Wibracje zegarka przy kaÅŧdej nowej lub zmienionej instrukcji nawigacji (tylko gdy aplikacja jest na pierwszym planie) + Redmi Watch 2 Lite + Stan połączenia + Redmi Smart Band Pro + Nothing Ear (2) + Nothing Ear (Stick) + Lekka aktywna redukcja szumÃŗw + Przezroczyste + Huawei Talk Band B6 + Huawei Watch GT 2e + Honor Band 4 + Honor Band 5 + Honor Band 7 + Huawei Watch GT 2 (Pro) + Huawei Band 4 (Pro) + Huawei Watch GT + Huawei Band 8 + Huawei Band 7 + Wyślij Åŧądanie odpluskwienia do urządzenia Huawei + Åģądanie odpluskwienia + HUAWEI TruSleep â„ĸ + Huawei Watch GT 3 (Pro) + Huawei Band 6 + Honor Band 3 + Honor Band 6 + Huawei Band (AW70) + Włącz automatyczne mierzenie SpO2 + Ręcznie + Połącz się ponownie tylko z połączonymi urządzeniami + Połącz się ponownie tylko z połączonymi urządzeniami, zamiast ponownego łączenia z wszystkimi urządzeniami + Ta opcja będzie działać dopiero w nowszej wersji aplikacji + Poziom baterii urządzenia jest zbyt niski + Cały dzień + NiektÃŗre urządzenia fałszywie twierdzą, Åŧe nie mają wsparcia dla pewnych funkcji. Tej opcji moÅŧna uÅŧyć by włączyć je pomimo tego. +\nKONTYNUUJ NA WŁASNE RYZYKO! +\nPrzeczytaj wiki + Powiadomienie na urządzeniu, po rozłączeniu Bluetooth. + Wymuś opcje + Nie przeszkadzać, gdy nie noszone + Tryb pracy + Nie odznaczaj inteligentnego wybudzania. + Nie zaznaczaj inteligentnego wybudzania. + Monitoruj jakość snu i sposÃŗb oddychania w czasie rzeczywistym. +\nPrzeanalizuj swoje wzorce snu i dokładnie zdiagnozuj 6 rodzajÃŗw problemÃŗw ze snem. + Wymuś inteligentny alarm + Ignoruj stan rozpoczęcia wybudzania + MoÅŧe pomÃŗc w prawidłowym wykrywaniu snu. Widoczne natychmiast w widoku codziennych aktywności. + Ignoruj stan zakończenia wybudzania + MoÅŧe pomÃŗc w prawidłowym wykrywaniu snu. Widoczne natychmiast w widoku codziennych aktywności. + Przeanalizuj ponownie dane treningu + Wymuś obsługę inteligentnych alarmÃŗw. +\nUÅģYWAJ NA WŁASNE RYZYKO + Wymuś lokalizację zuÅŧycia + Wymuszone wsparcie lokalizacji zuÅŧycia. +\nUÅģYWAJ NA WŁASNE RYZYKO + Intensywność brzęczyka + Jedynie gdy jest włączona opcja: podnieś, aby wybudzić ekran + Włącz odbieranie połączeń z poziomu urządzenia + Włącz odbieranie połączeń + Włącz odrzucanie połączeń + Włącz odrzucanie połączeń z poziomu urządzenia + Włącz automatyczny pomiar tętna + Wylącz funkcję ZnajdÅē mÃŗj telefon, gdy tryb Nie przeszkadzać jest włączony + Wymuś tryb Nie Przeszkadzać. +\nKONTYNUUJ NA WŁASNE RYZYKO! + Wymuś wspacie dla trybu Nie przeszkadzać + Poprawione monitorowanie snu + Hasło musi składać się z 4 znakÃŗw i zawierać jedynie cyfry + Płaski dystans + Wersja 1 + Wersja 2 + Wersja 3 + Xiaomi Watch S1 + Wersja protokołu + Xiaomi Watch S3 + Xiaomi Watch S1 Pro + Instalacja tarczy zegarka nie powiodła się + Instalacja tarczy zegarka zakończona + Wysyłanie tarczy zegarka + Wysyłanie tarczy zegarkaâ€Ļ + Wymuś typ połączenia + Xiaomi Smart Band 8 Pro + Mijia MHO-C303 + Info o aktywności + MoÅŧesz sprÃŗbować wymusić typ połączenia na wypadek, gdyby urządzenie nie odpowiedziało na Gadgetbridge + Automatycznie + Bluetooth LE + Bluetooth Classic + 155 uderzeń/min + 165 uderzeń/min + 175 uderzeń/min + 185 uderzeń/min + 195 uderzeń/min + 205 uderzeń/min + Orbitrek + Ergometr wioślarski + Crossfit + Trening fizyczny + Karate + Gra fitness + Aerobik + Pompki + Podciąganie się + Plank + Trampolina + Taniec jazzowy + Taniec latynoski + Balet + Inny taniec + Jazda na rolkach + Sztuki walki + Tai chi + Hula hop + Sporty z dyskiem + Rzutki + Łucznictwo + Jazda konno + Puszczanie latawca + Schody + Wędkarstwo + DrąÅŧek + Poręcze symetryczne + Rozciąganie + Kabaddi + Karting + Bilard + Futbol australijski + Pickleball + Åģeglarstwo + Hokej na lodzie + Sporty zimowe + Jazda na deskorolce + Wspinaczka skałkowa + Dwa ognie + Sony WI-SP600N + Honor MagicWatch 2 + CMF Watch Pro + Spacer na zewnątrz + Wyłącz przypomnienia o nawadnianiu dla zadanego przedziału czasu + PrÃŗg alarmowy tętna podczas intensywnej aktywności + Bieg wewnątrz + WędrÃŗwka gÃŗrska + Trening fitness + Taekwondo + Szermierka + Kendo + Polowanie + Przysiady + Softball \ No newline at end of file diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index b3fbb785a..91327b80a 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -2,20 +2,20 @@ Gadgetbridge Gadgetbridge - Setari - Depaneaza - Iesire - Doneaza - Sincronizeaza - Găsește brățara - Conecteaza - Deconecteaza - Sterge bratara - Sterge %1$s - Va fi stearsa bratara si datele asociate! - Deschide meniul de navigatie - Inchide meniul de navigatie - Apasati lung pentru a deconecta + Setări + Depanează + Ieșire + Donează + Sincronizează + Găsește dispozitivul pierdut + Se conecteazăâ€Ļ + Deconectează + Șterge dispozitiv + Șterge %1$s + Această opțiune va șterge dispozitivul și toate datele asociate cu acesta! + Deschide meniul de navigație + Închide meniul de navigație + Apăsați lung pe card pentru a deconecta Manager aplicatii Reinstaleaza Activeaza @@ -56,10 +56,10 @@ Afiseaza numele, ascunde numarul Ascunde numele si numarul Meteo - Captura ecran - Se deconecteaza - Conectareâ€Ļ - Capturez ecranul dispozitivului + Captură ecran + Se deconectează + Se conecteazăâ€Ļ + Fă o captură de ecran Depanare Aplicatii instalate Fete de ceas instalate @@ -107,8 +107,8 @@ Compas Setari Alipay - Modificare Culoare LED - Modificare Frecvență FM + Modifică culoarea LED-ului + Modifică frecvența FM Calibrare Dispozitiv Aplicație Redare Audio Preferată Ascunde notificarea Gadgetbridge @@ -134,4 +134,64 @@ Modul de economisire a energiei dezactivează măsurarea automată periodică a ritmului cardiac, crescÃĸnd astfel timpul de lucru Intervalul de alarmă inteligentă este intervalul ÃŽnainte de alarma instalată. În acest interval, dispozitivul ÃŽncearcă să detecteze cea mai ușoară fază de somn pentru a trezi utilizatorul Vibrație scăzută activată + Distanța este calculată din numărul de pași și valoarea lungimii pașilor (reglabile ÃŽn Setări - Despre tine) + Gadgetbridge (Nightly, fără funizor Pebble) + Oprire + Acuratețe + Gadgetbridge Nightly + Două brățări + Despre Bangle.js Gadgetbridge + 3 luni + Gadgetbridge Nightly fără Pebble + Bangle.js Gadgetbridge + Echilibrat + Somn + Setează poreclă + Toți sateliții + Acuratețea are prioritate + Despre Gadgetbridge Nightly fără Pebble + SăptămÃĸnă + GPS + 6 luni + Ești sigur că dorești să oprești acest dispozitiv ? + Zi + Informații despre activitate pe cardul dispozitivului + Despre Gadgetbridge Nightly + O singură brățară + Nightly NoPebble GB running + Două săptămÃĸni + Despre Bangle.js Gadgetbridge (Nightly) + An + Economisirea energiei + Caută %1$s? + Nightly Bangle.js rulează + Obține valorile ritmului cardiac + Bangle.js rulează + Arată numărul total de pași + Bangle.js Gadgetbridge (Nightly) + Arată durata somnului + Nivelul bateriei + Bangle.js Gadgetbridge + Lună + Arată informații despre activitate pe cardul dispozitivului + Informații despre baterie + Elimini preferințele dispozitivului? + Alege ce detalii vor fii afișate pe cardul dispozitivului + Înlocuitor sub o licență copyleft pentru aplicațiile cu sursă ÃŽnchisă Android, de administrare a unor dispozitive portabile. + GPS + GNOLASS + Nightly GB este pornit + Jurnal de modificări + Despre Gadgetbridge + Gadgetbridge este pornit + Oprire + GPS + BDS + GPS cu consum mic + Personalizat + Viteza are prioritate + Afișează numărul de pași, distanța sau somnul pe cardul dispozitivului + Bangle.js Gadgetbridge (Nightly) + Gadgetbridge (Nightly) + GPS + GALILEO + Chiar dorești o restaurare a setărilor din fabrică? + Xiaomi Watch S1 Active \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 374d3b065..7a3d6ab8d 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -50,7 +50,7 @@ НаŅŅ‚Ņ€ĐžĐšĐēи ОбŅ‰Đ¸Đĩ ĐŊĐ°ŅŅ‚Ņ€ĐžĐšĐēи - ПодĐēĐģŅŽŅ‡Đ°Ņ‚ŅŒŅŅ Đē дОйавĐģĐĩĐŊĐŊĐžĐŧŅƒ ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚вŅƒ ĐŋŅ€Đ¸ вĐēĐģŅŽŅ‡ĐĩĐŊии Bluetooth + ПодĐēĐģŅŽŅ‡Đ°Ņ‚ŅŒŅŅ Đē дОйавĐģĐĩĐŊĐŊŅ‹Đŧ ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚ваĐŧ ĐŋŅ€Đ¸ вĐēĐģŅŽŅ‡ĐĩĐŊии Bluetooth ЗаĐŋŅƒŅĐēĐ°Ņ‚ŅŒ авŅ‚ĐžĐŧĐ°Ņ‚иŅ‡ĐĩŅĐēи ПĐĩŅ€ĐĩĐŋОдĐēĐģŅŽŅ‡Đ°Ņ‚ŅŒŅŅ авŅ‚ĐžĐŧĐ°Ņ‚иŅ‡ĐĩŅĐēи ПŅ€ĐĩĐ´ĐŋĐžŅ‡Ņ‚иŅ‚ĐĩĐģŅŒĐŊŅ‹Đš ĐŧŅƒĐˇŅ‹ĐēĐ°ĐģŅŒĐŊŅ‹Đš ĐŋĐģĐĩĐĩŅ€ @@ -302,7 +302,7 @@ НĐĩŅĐžĐ˛ĐŧĐĩŅŅ‚иĐŧĐ°Ņ ĐŋŅ€ĐžŅˆĐ¸Đ˛ĐēĐ° Đ­Ņ‚Đ° ĐŋŅ€ĐžŅˆĐ¸Đ˛ĐēĐ° ĐŊĐĩ ŅĐžĐ˛ĐŧĐĩŅŅ‚иĐŧĐ° Ņ ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚вОĐŧ Đ ĐĩСĐĩŅ€Đ˛ĐŊŅ‹Đĩ ŅĐ¸ĐŗĐŊĐ°ĐģŅ‹ Đ´ĐģŅ ĐŋŅ€ĐĩĐ´ŅŅ‚ĐžŅŅ‰Đ¸Ņ… ŅĐžĐąŅ‹Ņ‚иК - ИŅĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ŅŒ Đ´Đ°Ņ‚Ņ‡Đ¸Đē ŅĐĩŅ€Đ´Ņ†ĐĩйиĐĩĐŊиŅ Đ´ĐģŅ ŅƒĐģŅƒŅ‡ŅˆĐĩĐŊиŅ ĐŧĐžĐŊиŅ‚ĐžŅ€Đ¸ĐŊĐŗĐ° ŅĐŊĐ° + ИŅĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ŅŒ Đ´Đ°Ņ‚Ņ‡Đ¸Đē ŅĐĩŅ€Đ´Ņ†ĐĩйиĐĩĐŊиŅ Đ´ĐģŅ ŅƒĐģŅƒŅ‡ŅˆĐĩĐŊиŅ ĐžŅ‚ŅĐģĐĩĐļиваĐŊиŅ ŅĐŊĐ° ĐĄĐŧĐĩŅ‰ĐĩĐŊиĐĩ вŅ€ĐĩĐŧĐĩĐŊи в Ņ‡Đ°ŅĐ°Ņ… (Đ´ĐģŅ Ņ‚ĐĩŅ…, ĐēŅ‚Đž Ņ€Đ°ĐąĐžŅ‚Đ°ĐĩŅ‚ ĐŋĐž ĐŊĐžŅ‡Đ°Đŧ) ФОŅ€ĐŧĐ°Ņ‚ Đ´Đ°Ņ‚Ņ‹ ВŅ€ĐĩĐŧŅ @@ -433,7 +433,7 @@ ШаĐŗОв в ĐŧиĐŊŅƒŅ‚Ņƒ ЧаŅŅ‹ ПŅƒĐģŅŒŅ - ЗаŅ€ŅĐ´ йаŅ‚Đ°Ņ€ĐĩĐšĐēи + ЗаŅ€ŅĐ´ йаŅ‚Đ°Ņ€Đĩи ДĐĩĐšŅŅ‚виŅ ĐēĐŊĐžĐŋĐēи НаŅŅ‚Ņ€ĐžĐšŅ‚Đĩ Đ´ĐĩĐšŅŅ‚виŅ ĐŋŅ€Đ¸ ĐŊĐ°ĐļĐ°Ņ‚ии ĐŊĐ° ĐēĐŊĐžĐŋĐēŅƒ КоĐģ-вО ĐŊĐ°ĐļĐ°Ņ‚иК ĐŊĐ° ĐēĐŊĐžĐŋĐēŅƒ @@ -460,8 +460,8 @@ НаŅ‡Đ°ĐģŅŒĐŊĐžĐĩ вŅ€ĐĩĐŧŅ ВŅ€ĐĩĐŧŅ ĐžĐēĐžĐŊŅ‡Đ°ĐŊиŅ АвŅ‚ĐžĐŧĐ°Ņ‚иŅ‡ĐĩŅĐēи - ĐŖĐŋŅ€ĐžŅ‰Ņ‘ĐŊĐŊŅ‹Đš ĐēиŅ‚Đ°ĐšŅĐēиК ŅĐˇŅ‹Đē - ĐĸŅ€Đ°Đ´Đ¸Ņ†Đ¸ĐžĐŊĐŊŅ‹Đš ĐēиŅ‚Đ°ĐšŅĐēиК ŅĐˇŅ‹Đē + КиŅ‚Đ°ĐšŅĐēиК ŅƒĐŋŅ€ĐžŅ‰Ņ‘ĐŊĐŊŅ‹Đš + КиŅ‚Đ°ĐšŅĐēиК Ņ‚Ņ€Đ°Đ´Đ¸Ņ†Đ¸ĐžĐŊĐŊŅ‹Đš АĐŊĐŗĐģиКŅĐēиК ŅĐˇŅ‹Đē ИСĐŧĐĩŅ€ĐĩĐŊиĐĩ ĐŋŅƒĐģŅŒŅĐ° в Ņ‚ĐĩŅ‡ĐĩĐŊиĐĩ Đ´ĐŊŅ Ņ€Đ°Đˇ в ĐŧиĐŊŅƒŅ‚Ņƒ @@ -566,12 +566,12 @@ КаĐģийŅ€ĐžĐ˛ĐēĐ° Watch 9 РаСйĐģĐžĐēиŅ€ĐžĐ˛ĐēĐ° ŅĐēŅ€Đ°ĐŊĐ° ĐĄĐŧĐ°Ņ…ĐŊиŅ‚Đĩ ввĐĩŅ€Ņ… Đ´ĐģŅ Ņ€Đ°ĐˇĐąĐģĐžĐēиŅ€ĐžĐ˛Đēи ŅĐēŅ€Đ°ĐŊĐ° ĐąŅ€Đ°ŅĐģĐĩŅ‚Đ° - НĐĩĐŧĐĩŅ†ĐēиК ŅĐˇŅ‹Đē - ИŅ‚Đ°ĐģŅŒŅĐŊŅĐēиК ŅĐˇŅ‹Đē - ФŅ€Đ°ĐŊŅ†ŅƒĐˇŅĐēиК ŅĐˇŅ‹Đē - ПоĐģŅŒŅĐēиК ŅĐˇŅ‹Đē - КоŅ€ĐĩĐšŅĐēиК ŅĐˇŅ‹Đē - Đ¯ĐŋĐžĐŊŅĐēиК ŅĐˇŅ‹Đē + НĐĩĐŧĐĩŅ†ĐēиК + ИŅ‚Đ°ĐģŅŒŅĐŊŅĐēиК + ФŅ€Đ°ĐŊŅ†ŅƒĐˇŅĐēиК + ПоĐģŅŒŅĐēиК + КоŅ€ĐĩĐšŅĐēиК + Đ¯ĐŋĐžĐŊŅĐēиК НоŅ€Đ˛ĐĩĐļŅĐēиК (ĐģиŅ‚ĐĩŅ€Đ°Ņ‚ŅƒŅ€ĐŊŅ‹Đš) ПодĐĩĐģиŅ‚ŅŒŅŅ ĐģĐžĐŗ-Ņ„Đ°ĐšĐģĐžĐŧ ФаКĐģŅ‹ ĐļŅƒŅ€ĐŊĐ°ĐģĐ° (log-Ņ„Đ°ĐšĐģŅ‹) Gadgetbridge ĐŧĐžĐŗŅƒŅ‚ ŅĐžĐ´ĐĩŅ€ĐļĐ°Ņ‚ŅŒ Ņ€Đ°ĐˇĐģиŅ‡ĐŊŅƒŅŽ ĐģиŅ‡ĐŊŅƒŅŽ иĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸ŅŽ, Ņ‚Đ°ĐēŅƒŅŽ ĐēĐ°Đē ŅƒĐŊиĐēĐ°ĐģŅŒĐŊŅ‹Đĩ идĐĩĐŊŅ‚иŅ„иĐēĐ°Ņ‚ĐžŅ€Ņ‹ (ĐŊĐ°ĐŋŅ€Đ¸ĐŧĐĩŅ€, MAC-Đ°Đ´Ņ€ĐĩŅ ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚ва), иĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸ŅŽ Đž ĐŧŅƒĐˇŅ‹ĐēĐ°ĐģŅŒĐŊŅ‹Ņ… ĐŋŅ€ĐĩĐ´ĐŋĐžŅ‡Ņ‚ĐĩĐŊиŅŅ… и Ņ‚.Đ´. Đ ĐĩĐēĐžĐŧĐĩĐŊĐ´ŅƒĐĩŅ‚ŅŅ Ņ€ĐĩĐ´Đ°ĐēŅ‚иŅ€ĐžĐ˛Đ°ĐŊиĐĩ Ņ„Đ°ĐšĐģĐ° ĐļŅƒŅ€ĐŊĐ°ĐģĐ° и ŅƒĐ´Đ°ĐģĐĩĐŊиĐĩ ĐģиŅ‡ĐŊОК иĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸Đ¸ ĐŋĐĩŅ€ĐĩĐ´ ĐžŅ‚ĐŋŅ€Đ°Đ˛ĐēОК в ĐŋŅƒĐąĐģиŅ‡ĐŊĐžĐŧ ĐžŅ‚Ņ‡ĐĩŅ‚Đĩ Đž ĐŋŅ€ĐžĐąĐģĐĩĐŧĐĩ. @@ -707,14 +707,14 @@ \nВĐĢ ДЕЙСĐĸВĐŖЕĐĸЕ НА СВОЙ ĐĄĐĸРАĐĨ И РИСК! \n \nПОЛНОСĐĸĐŦĐŽ НЕ ПРОĐĸЕСĐĸИРОВАНО. ВОЗМОЖНО, НĐŖЖНО ПРОШИĐĸĐŦ ПРОШИВКĐŖ BEATS_W, ЕСЛИ Đ˜ĐœĐ¯ ВАШЕГО ĐŖĐĄĐĸРОЙСĐĸВА \"Amazfit Band 2\" - ГоĐģĐģĐ°ĐŊĐ´ŅĐēиК ŅĐˇŅ‹Đē - ĐĸŅƒŅ€ĐĩŅ†ĐēиК ŅĐˇŅ‹Đē - ĐŖĐēŅ€Đ°Đ¸ĐŊŅĐēиК ŅĐˇŅ‹Đē - АŅ€Đ°ĐąŅĐēиК ŅĐˇŅ‹Đē - ИĐŊĐ´ĐžĐŊĐĩСиКŅĐēиК ŅĐˇŅ‹Đē - ĐĸĐ°ĐšŅĐēиК ŅĐˇŅ‹Đē - ВŅŒĐĩŅ‚ĐŊĐ°ĐŧŅĐēиК ŅĐˇŅ‹Đē - ПоŅ€Ņ‚ŅƒĐŗĐ°ĐģŅŒŅĐēиК ŅĐˇŅ‹Đē + ГоĐģĐģĐ°ĐŊĐ´ŅĐēиК + ĐĸŅƒŅ€ĐĩŅ†ĐēиК + ĐŖĐēŅ€Đ°Đ¸ĐŊŅĐēиК + АŅ€Đ°ĐąŅĐēиК + ИĐŊĐ´ĐžĐŊĐĩСиКŅĐēиК + ĐĸĐ°ĐšŅĐēиК + ВŅŒĐĩŅ‚ĐŊĐ°ĐŧŅĐēиК + ПоŅ€Ņ‚ŅƒĐŗĐ°ĐģŅŒŅĐēиК Amazfit Cor 2 Mi Band 4 ВŅ‹ ŅĐžĐąĐ¸Ņ€Đ°ĐĩŅ‚ĐĩŅŅŒ ŅƒŅŅ‚Đ°ĐŊОвиŅ‚ŅŒ ĐŋŅ€ĐžŅˆĐ¸Đ˛ĐēŅƒ %s ĐŊĐ° ваŅˆ Mi Band 4. @@ -1533,7 +1533,7 @@ ЛĐĩвŅ‹Đš ĐŊĐ°ŅƒŅˆĐŊиĐē 2-ОК Ņ‡Đ°ŅĐžĐ˛ĐžĐš ĐŋĐžŅŅ ВĐĩŅ€ĐžŅŅ‚ĐŊĐžŅŅ‚ŅŒ Đ´ĐžĐļĐ´Ņ - НĐĩ ОйĐŊОвĐģŅŅ‚ŅŒ иĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸ŅŽ ĐŊĐ° ŅĐēŅ€Đ°ĐŊĐĩ, ĐēĐžĐŗĐ´Đ° ŅƒŅŅ‚Ņ€ĐžŅŅ‚вО ĐŊĐĩ ĐŊĐ°Đ´ĐĩŅ‚Đž + НĐĩ ОйĐŊОвĐģŅŅ‚ŅŒ иĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸ŅŽ ĐŊĐ° ŅĐēŅ€Đ°ĐŊĐĩ, ĐēĐžĐŗĐ´Đ° ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚вО ŅĐŊŅŅ‚Đž НоŅ€ĐŧĐ°ĐģŅŒĐŊŅ‹Đš ĐŖŅĐ¸ĐģĐĩĐŊиĐĩ йаŅĐžĐ˛ ВибŅ€Đ°Ņ†Đ¸Ņ ĐŋŅ€Đ¸ ĐŋĐžŅŅ‚ŅƒĐŋĐģĐĩĐŊии СвОĐŊĐēОв, ŅĐžĐžĐąŅ‰ĐĩĐŊиК, ĐžĐŋОвĐĩŅ‰ĐĩĐŊиК и ĐŋŅ€ĐžŅ‡ĐĩĐŗĐž @@ -1604,7 +1604,7 @@ ĐŖŅĐ¸ĐģĐĩĐŊиĐĩ ВЧ ДиĐŊĐ°ĐŧиŅ‡ĐŊŅ‹Đš ПодĐĩĐģиŅ‚ŅŒŅŅ - ВŅ‹ĐēĐģŅŽŅ‡ĐĩĐŊĐž + ВŅ‹ĐēĐģ. ОŅ‚ĐŋŅ€Đ°Đ˛ĐēĐ° GPS вО вŅ€ĐĩĐŧŅ Ņ‚Ņ€ĐĩĐŊиŅ€ĐžĐ˛Đēи ЗаĐŋŅƒŅĐē/ĐžŅŅ‚Đ°ĐŊОвĐēĐ° ĐžŅ‚ŅĐģĐĩĐļиваĐŊиŅ Ņ„иŅ‚ĐŊĐĩŅ-ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊиŅ ĐŊĐ° Ņ‚ĐĩĐģĐĩŅ„ĐžĐŊĐĩ ĐŋŅ€Đ¸ СаĐŋŅƒŅ‰ĐĩĐŊĐŊОК GPS-Ņ‚Ņ€ĐĩĐŊиŅ€ĐžĐ˛ĐēĐĩ ĐŊĐ° ĐąŅ€Đ°ŅĐģĐĩŅ‚Đĩ ОĐŋОвĐĩŅ‰ĐĩĐŊиŅ Đž ĐŋŅ€ĐžŅŅ‚ĐžĐĩ @@ -1690,7 +1690,7 @@ ĐĄĐžĐģĐŊŅ†Đĩ и ЛŅƒĐŊĐ° ĐĸĐĩĐģĐĩŅ„ĐžĐŊ Đ­ĐēŅ€Đ°ĐŊ ĐąĐģĐžĐēиŅ€ĐžĐ˛Đēи - ПаŅ€ĐžĐģŅŒ Đ´ĐžĐģĐļĐĩĐŊ ŅĐžŅŅ‚ĐžŅŅ‚ŅŒ иС 6 Ņ†Đ¸Ņ„Ņ€, иŅĐŋĐžĐģŅŒĐˇŅƒŅ Ņ‚ĐžĐģŅŒĐēĐž Ņ†Đ¸Ņ„Ņ€Ņ‹ + ПаŅ€ĐžĐģŅŒ Đ´ĐžĐģĐļĐĩĐŊ ŅĐžŅŅ‚ĐžŅŅ‚ŅŒ иС 6 Ņ†Đ¸Ņ„Ņ€ БĐģĐžĐēиŅ€ĐžĐ˛ĐēĐ° ĐąŅ€Đ°ŅĐģĐĩŅ‚Đ° ĐŋĐ°Ņ€ĐžĐģĐĩĐŧ ĐŋŅ€Đ¸ ŅĐŊŅŅ‚ии Ņ СаĐŋŅŅŅ‚ŅŒŅ ИĐŊŅ‚ĐĩĐŊŅĐ¸Đ˛ĐŊĐ°Ņ иĐŊŅ‚ĐĩŅ€Đ˛Đ°ĐģŅŒĐŊĐ°Ņ Ņ‚Ņ€ĐĩĐŊиŅ€ĐžĐ˛ĐēĐ° ПŅ€ĐĩĐ´ŅƒŅŅ‚Đ°ĐŊОвĐēи @@ -1758,7 +1758,7 @@ ĐĄĐēĐ°Ņ‡Đ°Ņ‚ŅŒ в ĐēŅŅˆ ВĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒ ĐŊĐĩĐŋОддĐĩŅ€ĐļиваĐĩĐŧŅ‹Đĩ ĐŊĐ°ŅŅ‚Ņ€ĐžĐšĐēи НаŅŅ‚Ņ€ĐžĐ¸Ņ‚ŅŒ ĐžŅ‚ŅĐģĐĩĐļиваĐŊиĐĩ ŅĐĩŅ€Đ´ĐĩŅ‡ĐŊĐžĐŗĐž Ņ€Đ¸Ņ‚ĐŧĐ° - ВŅĐĩĐŗĐ´Đ° ĐŊĐ° ŅĐēŅ€Đ°ĐŊĐĩ + Đ­ĐēŅ€Đ°ĐŊ вŅĐĩĐŗĐ´Đ° вĐēĐģŅŽŅ‡ĐĩĐŊ ВŅĐĩĐŗĐ´Đ° Đ´ĐĩŅ€ĐļĐ°Ņ‚ŅŒ ŅĐēŅ€Đ°ĐŊ ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚ва вĐēĐģŅŽŅ‡ĐĩĐŊĐŊŅ‹Đŧ ЧŅƒĐ˛ŅŅ‚виŅ‚ĐĩĐģŅŒĐŊĐžŅŅ‚ŅŒ ЗŅƒĐŧйа @@ -1955,7 +1955,7 @@ Amazfit T-Rex 2 КоĐŊŅ‚Đ°ĐēŅ‚Ņ‹ ĐŖĐ´Đ°ĐģиŅ‚ŅŒ ĐēĐžĐŊŅ‚Đ°ĐēŅ‚ - ЕĐļĐĩĐ´ĐŊĐĩвĐŊĐ°Ņ Ņ†ĐĩĐģŅŒ: вŅ€ĐĩĐŧŅ ŅŅ‚ĐžŅĐŊиŅ в ĐŧиĐŊŅƒŅ‚Đ°Ņ… + ЕĐļĐĩĐ´ĐŊĐĩвĐŊĐ°Ņ Ņ†ĐĩĐģŅŒ: вŅ€ĐĩĐŧŅ ŅŅ‚ĐžŅ в ĐŧиĐŊŅƒŅ‚Đ°Ņ… ĐĄŅ‚иĐģŅŒ ĐĨĐžŅ€Đ˛Đ°Ņ‚ŅĐēиК ЕĐļĐĩĐ´ĐŊĐĩвĐŊĐ°Ņ Ņ†ĐĩĐģŅŒ: вŅ€ĐĩĐŧŅ ŅĐļиĐŗĐ°ĐŊиŅ ĐļиŅ€Đ° в ĐŧиĐŊŅƒŅ‚Đ°Ņ… @@ -1971,7 +1971,7 @@ МиĐŊŅƒŅ‚Ņ‹ Đ°ĐēŅ‚ивĐŊĐžŅŅ‚и ĐŋĐĩŅ€ĐĩĐ´ ОйĐŊĐ°Ņ€ŅƒĐļĐĩĐŊиĐĩĐŧ КоĐģиŅ‡ĐĩŅŅ‚вО ĐŧиĐŊŅƒŅ‚, в Ņ‚ĐĩŅ‡ĐĩĐŊиĐĩ ĐēĐžŅ‚ĐžŅ€Ņ‹Ņ… Ņ‚Ņ€ĐĩĐŊиŅ€ĐžĐ˛ĐēĐ° Đ´ĐžĐģĐļĐŊĐ° ĐŋŅ€ĐžĐ´ĐžĐģĐļĐ°Ņ‚ŅŒŅŅ, ĐŋŅ€ĐĩĐļĐ´Đĩ Ņ‡ĐĩĐŧ ĐžĐŊĐ° ĐąŅƒĐ´ĐĩŅ‚ ОйĐŊĐ°Ņ€ŅƒĐļĐĩĐŊĐ° НаŅŅ‚Ņ€ĐžĐ¸Ņ‚ŅŒ ĐēĐžĐŊŅ‚Đ°ĐēŅ‚Ņ‹ ĐŊĐ° Ņ‡Đ°ŅĐ°Ņ… - ВŅ‹ ŅƒĐ˛ĐĩŅ€ĐĩĐŊŅ‹ Ņ‡Ņ‚Đž Ņ…ĐžŅ‚иŅ‚Đĩ ŅƒĐ´Đ°ĐģиŅ‚ŅŒ ĐēĐžĐŊŅ‚Đ°ĐēŅ‚ \'%1$s\'\? + ВŅ‹ Ņ‚ĐžŅ‡ĐŊĐž Ņ…ĐžŅ‚иŅ‚Đĩ ŅƒĐ´Đ°ĐģиŅ‚ŅŒ ĐēĐžĐŊŅ‚Đ°ĐēŅ‚ ÂĢ%1$sÂģ? КоĐŊŅ‚Đ°ĐēŅ‚ĐŊĐ°Ņ иĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸Ņ ФаКĐģ ĐŊĐĩ ĐŧĐžĐļĐĩŅ‚ ĐąŅ‹Ņ‚ŅŒ ŅƒŅŅ‚Đ°ĐŊОвĐģĐĩĐŊ, ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚вО ĐŊĐĩ ĐŋОддĐĩŅ€ĐļиваĐĩŅ‚ŅŅ. НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ĐŋОдĐĩĐģиŅ‚ŅŒŅŅ Ņ„Đ°ĐšĐģĐžĐŧ. @@ -2039,8 +2039,8 @@ ПодĐĩĐģиŅ‚ŅŒŅŅ НĐĩОйŅ€Đ°ĐąĐžŅ‚Đ°ĐŊĐŊОК ХвОдĐēОК ПодĐĩĐģиŅ‚ŅŒŅŅ НĐĩОйŅ€Đ°ĐąĐžŅ‚Đ°ĐŊĐŊŅ‹Đŧи ДĐĩŅ‚Đ°ĐģŅĐŧи ИĐŊŅŅ‚Ņ€ŅƒĐŧĐĩĐŊŅ‚Ņ‹ РаСŅ€Đ°ĐąĐžŅ‚Ņ‡Đ¸ĐēĐ° - ĐĄŅ‚иĐģŅŒ ĐŋОвŅ‚ĐžŅ€ŅĐĩŅ‚ Ņ†Đ¸Ņ„ĐĩŅ€ĐąĐģĐ°Ņ‚ Ņ‡Đ°ŅĐžĐ˛ - ВŅ‹ ŅƒĐ˛ĐĩŅ€ĐĩĐŊŅ‹, Ņ‡Ņ‚Đž Ņ…ĐžŅ‚иŅ‚Đĩ ŅƒĐ´Đ°ĐģиŅ‚ŅŒ %d Đ°ĐēŅ‚ивĐŊĐžŅŅ‚ĐĩĐš\? + ĐĄŅ‚иĐģŅŒ ĐēĐ°Đē ĐŊĐ° Ņ†Đ¸Ņ„ĐĩŅ€ĐąĐģĐ°Ņ‚Đĩ + ВŅ‹ Ņ‚ĐžŅ‡ĐŊĐž Ņ…ĐžŅ‚иŅ‚Đĩ ŅƒĐ´Đ°ĐģиŅ‚ŅŒ %d Đ°ĐēŅ‚ивĐŊĐžŅŅ‚и(-ĐĩĐš)? ОŅ„Ņ„ĐģĐ°ĐšĐŊ ĐŗĐžĐģĐžŅ ĐĸŅ€Đ°ĐŊŅĐģиŅ€ĐžĐ˛Đ°Ņ‚ŅŒ ИĐŊŅ‚ĐĩĐŊŅ‚Ņ‹ МĐĩдиа-ĐēĐŊĐžĐŋĐžĐē НаĐŋŅ€ŅĐŧŅƒŅŽ ВĐēĐģŅŽŅ‡Đ¸Ņ‚Đĩ, ĐĩŅĐģи ŅƒĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ ĐŧĐĩдиа ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚ва ĐŊĐĩ Ņ€Đ°ĐąĐžŅ‚Đ°ĐĩŅ‚ Đ´ĐģŅ ĐžĐŋŅ€ĐĩĐ´ĐĩĐģŅ‘ĐŊĐŊŅ‹Ņ… ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊиК @@ -2119,7 +2119,7 @@ \nВĐŊиĐŧĐ°ĐŊиĐĩ: ĐĩŅĐģи ŅƒŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐž ĐžŅ„иŅ†Đ¸Đ°ĐģŅŒĐŊĐžĐĩ ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊиĐĩ Fossil, Đ´ĐžĐģĐŗĐžĐĩ СаĐļĐ°Ņ‚иĐĩ вĐĩŅ€Ņ…ĐŊĐĩĐš ĐēĐŊĐžĐŋĐēи Ņ‚Đ°ĐēĐļĐĩ ĐąŅƒĐ´ĐĩŅ‚ ĐŋĐĩŅ€ĐĩĐēĐģŅŽŅ‡Đ°Ņ‚ŅŒ ĐžŅ‚ОйŅ€Đ°ĐļĐĩĐŊиĐĩ видĐļĐĩŅ‚Ов. ПоĐģŅƒŅ‡ĐĩĐŊиĐĩ ŅĐ˛ĐžĐ´Đēи ĐŋĐž ŅĐŋĐžŅ€Ņ‚Ņƒ ПоĐģŅƒŅ‡ĐĩĐŊиĐĩ Đ´Đ°ĐŊĐŊŅ‹Ņ… иĐŊĐ´ĐĩĐēŅĐ° Ņ„иС. Đ°ĐēŅ‚ивĐŊĐžŅŅ‚и - ПоĐģŅƒŅ‡ĐĩĐŊиĐĩ Đ´Đ°ĐŊĐŊŅ‹Ņ… Đž ĐŊĐ°ŅŅ‹Ņ‰ĐĩĐŊĐŊии ĐēŅ€ĐžĐ˛Đ¸ ĐēиŅĐģĐžŅ€ĐžĐ´ĐžĐŧ + ПоĐģŅƒŅ‡ĐĩĐŊиĐĩ Đ´Đ°ĐŊĐŊŅ‹Ņ… Đž ĐēиŅĐģĐžŅ€ĐžĐ´Đĩ в ĐēŅ€ĐžĐ˛Đ¸ ПоĐģŅƒŅ‡ĐĩĐŊиĐĩ Đ´Đ°ĐŊĐŊŅ‹Ņ… Đž ŅĐĩŅ€Ņ†ĐĩйиĐĩĐŊии ЖŅƒŅ€ĐŊĐ°Đģ ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊиŅ ВĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒ вĐĩĐ´ĐĩĐŊиĐĩ ĐļŅƒŅ€ĐŊĐ°ĐģĐ° иС ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊиŅ Ņ‡Đ°ŅĐžĐ˛ @@ -2155,7 +2155,7 @@ АвŅ‚ĐžĐŧĐ°Ņ‚иŅ‡ĐĩŅĐēи ĐŖĐŧĐĩŅ€ĐĩĐŊĐŊŅ‹Đš ВŅ‹ŅĐžĐēиК - ОбŅ‰Đ¸Đš иĐŊĐ´ĐĩĐēŅ Ņ„иС. Đ°ĐēŅ‚ивĐŊĐžŅŅ‚и + ВŅĐĩĐŗĐž АдаĐŋŅ‚ивĐŊŅ‹Đĩ Ņ†Đ˛ĐĩŅ‚Đ° Casio GMW-B5000 ИĐŊĐ´ĐĩĐēŅ ĐŖФ-иСĐģŅƒŅ‡ĐĩĐŊиŅ @@ -2365,7 +2365,7 @@ ЗаŅŅ‡Đ¸Ņ‚Ņ‹Đ˛Đ°Ņ‚ŅŒ двОКĐŊĐžĐĩ ĐēĐ°ŅĐ°ĐŊиĐĩ, Đ´Đ°ĐļĐĩ ĐĩŅĐģи ĐžĐŊĐž ĐąŅ‹ĐģĐž ĐŋĐž ŅĐēŅ€Đ°ĐŊŅƒ ВŅ€ĐĩĐŧŅ, ĐēĐžĐŗĐ´Đ° ŅĐžĐąĐ¸Ņ€Đ°ĐĩŅ‚ŅŅ иĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸Ņ Đž ŅĐŊĐĩ ЗавĐĩŅ€ŅˆĐ¸Ņ‚ŅŒ ĐŋĐžŅĐģĐĩ Ņ‚иŅˆĐ¸ĐŊŅ‹ ĐŊĐ° ĐŋŅ€ĐžŅ‚ŅĐļĐĩĐŊии: - ДĐŊĐĩвĐŊĐžĐĩ ŅƒĐ˛ĐĩĐģиŅ‡ĐĩĐŊиĐĩ иĐŊĐ´ĐĩĐēŅĐ° Ņ„иСиŅ‡ĐĩŅĐēОК Đ°ĐēŅ‚ивĐŊĐžŅŅ‚и + ПŅ€Đ¸Ņ€ĐžŅŅ‚ Са Đ´ĐĩĐŊŅŒ ИŅĐŋĐžĐģŅŒĐˇŅƒĐšŅ‚Đĩ ĐŊĐ°ŅƒŅˆĐŊиĐēи ĐŋĐž-ОйŅ‹Ņ‡ĐŊĐžĐŧŅƒ. ЕŅĐģи ŅƒŅĐģОвиŅ ĐŊĐžŅˆĐĩĐŊиŅ иĐģи Đ°Ņ‚ĐŧĐžŅŅ„ĐĩŅ€ĐŊĐžĐĩ давĐģĐĩĐŊиĐĩ иСĐŧĐĩĐŊŅŅ‚ŅŅ, СаĐŋŅƒŅŅ‚иŅ‚Đĩ ĐžĐŋŅ‚иĐŧиСаŅ†Đ¸ŅŽ ĐĩŅ‰Ņ‘ Ņ€Đ°Đˇ. РаСĐŗОвОŅ€ ĐŋŅ€Đ¸ ĐŊĐžŅˆĐĩĐŊии ĐŗĐ°Ņ€ĐŊиŅ‚ŅƒŅ€Ņ‹ ХвОŅ ĐŋŅ€ĐĩĐ´ŅƒŅŅ‚Đ°ĐŊОвĐēĐ° 1 @@ -2514,7 +2514,7 @@ ĐĸĐĩŅ€ĐŧĐžĐŧĐĩŅ‚Ņ€ Femometer Vinca II НавиĐŗĐ°Ņ†Đ¸Ņ ĐąŅ‹ĐģĐ° ĐŊĐ°Ņ‡Đ°Ņ‚Đ°, ĐŊĐž navigationApp ĐŊĐĩ ŅƒŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐž ĐŊĐ° Ņ‡Đ°ŅĐ°Ņ…. ПоĐļĐ°ĐģŅƒĐšŅŅ‚Đ°, ŅƒŅŅ‚Đ°ĐŊОвиŅ‚Đĩ ĐĩĐŗĐž в ĐŧĐĩĐŊĐĩĐ´ĐļĐĩŅ€Đĩ ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊиК. - ПŅ€Đ¸ĐģĐžĐļĐĩĐŊиĐĩ ĐŊавиĐŗĐ°Ņ†Đ¸Đ¸ ĐŊĐĩ ŅƒŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐž ĐŊĐ° Ņ‡Đ°ŅĐ°Ņ… + ПŅ€Đ¸ĐģĐžĐļĐĩĐŊиĐĩ Đ´ĐģŅ ĐŊавиĐŗĐ°Ņ†Đ¸Đ¸ ĐŊĐĩ ŅƒŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐž ĐŊĐ° Ņ‡Đ°ŅĐ°Ņ… ИĐŗĐŊĐžŅ€Đ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ (ĐŋŅ€ĐĩĐēŅ€Đ°Ņ‚иŅ‚ŅŒ ŅƒĐ˛ĐĩĐ´ĐžĐŧĐģĐĩĐŊиĐĩ) ОŅ‚ĐēĐģĐžĐŊиŅ‚ŅŒ ĐĄĐŋĐžŅĐžĐą ĐžŅ‚ĐēĐģĐžĐŊĐĩĐŊиŅ вŅ‹ĐˇĐžĐ˛Đ° @@ -2523,4 +2523,234 @@ КаĐēĐžĐĩ Đ´ĐĩĐšŅŅ‚виĐĩ ĐŊĐĩОйŅ…ОдиĐŧĐž ŅĐžĐ˛ĐĩŅ€ŅˆĐ¸Ņ‚ŅŒ ĐŋŅ€Đ¸ ĐžŅ‚ĐēĐģĐžĐŊĐĩĐŊии вŅ‹ĐˇĐžĐ˛Đ° Ņ Ņ‡Đ°ŅĐžĐ˛ ПоĐģŅƒŅ‡ĐĩĐŊиĐĩ Ņ‚ĐĩĐŧĐŋĐĩŅ€Đ°Ņ‚ŅƒŅ€Ņ‹ ДĐģиĐŊĐ° ĐŋĐžĐģĐžŅŅ‹ + Amazfit Active Edge + ĐĄŅ€ĐĩĐ´ĐŊиК ŅƒŅ€ĐžĐ˛ĐĩĐŊŅŒ ĐēиŅĐģĐžŅ€ĐžĐ´Đ° в ĐēŅ€ĐžĐ˛Đ¸ + Amazfit Active + НавиĐŗĐ°Ņ†Đ¸ĐžĐŊĐŊŅ‹Đĩ ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊиŅ + НаŅŅ‚Ņ€ĐžĐšĐēи ĐŊавиĐŗĐ°Ņ†Đ¸Đ¸ + Google КаŅ€Ņ‚Ņ‹ + НаŅ‡Đ¸ĐŊĐ°Ņ‚ŅŒ СаĐŗĐžĐģОвОĐē ŅƒĐ˛ĐĩĐ´ĐžĐŧĐģĐĩĐŊиŅ Ņ ĐŊаСваĐŊиŅ ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊиŅ-иŅŅ‚ĐžŅ‡ĐŊиĐēĐ° + ИŅĐŋĐžĐģŅŒĐˇŅƒĐĩŅ‚ŅŅ Đ´ĐģŅ вŅ‹ĐąĐžŅ€Đ° иŅĐŋĐžĐģŅŒĐˇŅƒĐĩĐŧОК вĐĩŅ€ŅĐ¸Đ¸ OsmAnd + НазваĐŊиĐĩ ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊиŅ в ŅƒĐ˛ĐĩĐ´ĐžĐŧĐģĐĩĐŊии + ИĐŧŅ ĐŋĐ°ĐēĐĩŅ‚Đ° OsmAnd + OsmAnd(+) + ОĐŋОвĐĩŅ‰ĐĩĐŊиŅ ĐŋŅ€ĐžŅ‡Đ¸Ņ… ŅƒĐ˛ĐĩĐ´ĐžĐŧĐģĐĩĐŊиК + ОĐŋОвĐĩŅ‰ĐĩĐŊиŅ ĐēĐ°ĐģĐĩĐŊĐ´Đ°Ņ€Ņ + ОĐŋОвĐĩŅ‰Đ°Ņ‚ŅŒ Đž вŅ…ОдŅŅ‰Đ¸Ņ… СвОĐŊĐēĐ°Ņ… вийŅ€Đ°Ņ†Đ¸ĐĩĐš/ĐŋиŅ‰Đ°ĐŊиĐĩĐŧ + Xiaomi Smart Band 8 + БĐĩĐŗ + Đ Đ°ŅĐŋиŅĐ°ĐŊиĐĩ Ņ€ĐĩĐļиĐŧĐ° ŅĐŊĐ° + ОĐŋОвĐĩŅ‰ĐĩĐŊиŅ Đž вŅ…ОдŅŅ‰Đ¸Ņ… + НаŅ‡Đ°ĐģĐž ŅĐŊĐ° + ОĐŋОвĐĩŅ‰ĐĩĐŊиŅ ŅĐģ. ĐŋĐžŅ‡Ņ‚Ņ‹ + ОĐŋОвĐĩŅ‰Đ°Ņ‚ŅŒ Đž ĐŊОвŅ‹Ņ… ĐŋиŅŅŒĐŧĐ°Ņ… вийŅ€Đ°Ņ†Đ¸ĐĩĐš/ĐŋиŅ‰Đ°ĐŊиĐĩĐŧ + КоĐŊŅ†ĐĩĐŊŅ‚Ņ€Đ°Ņ†Đ¸Ņ + ОĐŋОвĐĩŅ‰Đ°Ņ‚ŅŒ Đž ĐŋĐžĐģŅƒŅ‡ĐĩĐŊĐŊŅ‹Ņ… SMS (Ņ‚ĐĩĐēŅŅ‚ОвŅ‹Ņ…) ŅĐžĐžĐąŅ‰ĐĩĐŊиŅŅ… вийŅ€Đ°Ņ†Đ¸ĐĩĐš/ĐŋиŅ‰Đ°ĐŊиĐĩĐŧ + ПоĐēаСŅ‹Đ˛Đ°Ņ‚ŅŒ ŅĐžĐžĐąŅ‰ĐĩĐŊиĐĩ в СаĐŗĐžĐģОвĐēĐĩ + Redmi Watch 3 Active + ОĐŋОвĐĩŅ‰Đ°Ņ‚ŅŒ Đž ŅĐžĐąŅ‹Ņ‚иŅŅ… ĐēĐ°ĐģĐĩĐŊĐ´Đ°Ņ€Ņ вийŅ€Đ°Ņ†Đ¸ĐĩĐš/ĐŋиŅ‰Đ°ĐŊиĐĩĐŧ + ĐĄĐĩŅ€Đ¸ĐšĐŊŅ‹Đš ĐŊĐžĐŧĐĩŅ€ + ДаŅ‚ŅĐēиК + ПŅ€ĐžĐąŅƒĐļĐ´ĐĩĐŊиĐĩ + ĐĄŅ‚Đ°Ņ‚иŅŅ‚иĐēĐ° + ОĐŋОвĐĩŅ‰Đ°Ņ‚ŅŒ ĐŋŅ€Đ¸ ĐŋŅ€ĐžŅ‡Đ¸Ņ… ŅƒĐ˛ĐĩĐ´ĐžĐŧĐģĐĩĐŊиŅŅ… вийŅ€Đ°Ņ†Đ¸ĐĩĐš/ĐŋиŅ‰Đ°ĐŊиĐĩĐŧ + Xiaomi Watch Lite + ПоĐēаСŅ‹Đ˛Đ°Ņ‚ŅŒ ŅĐžĐžĐąŅ‰ĐĩĐŊиĐĩ в СаĐŗĐžĐģОвĐēĐĩ ŅƒĐ˛ĐĩĐ´ĐžĐŧĐģĐĩĐŊиŅ в ŅĐžĐžŅ‚вĐĩŅ‚ŅŅ‚вии Ņ ĐŊĐ°ŅŅ‚Ņ€ĐžĐšĐēĐ°Đŧи ĐēĐžĐŊŅ„идĐĩĐŊŅ†Đ¸Đ°ĐģŅŒĐŊĐžŅŅ‚и ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚ва + ОŅ‚ĐŋŅ€Đ°Đ˛Đ¸Ņ‚ŅŒ ĐŊĐ°ĐŋĐžĐŧиĐŊĐ°ĐŊиĐĩ и вОКŅ‚и в Ņ€ĐĩĐļиĐŧ ŅĐŊĐ° вО вŅ€ĐĩĐŧŅ ĐŊĐ°Ņ‡Đ°ĐģĐ° ŅĐŊĐ°. АĐēŅ‚ивиŅ€ĐžĐ˛Đ°Ņ‚ŅŒ ĐąŅƒĐ´Đ¸ĐģŅŒĐŊиĐē ĐŋŅ€Đ¸ СадаĐŊĐŊĐžĐŧ вŅ€ĐĩĐŧĐĩĐŊи ĐŋŅ€ĐžĐąŅƒĐļĐ´ĐĩĐŊиŅ. + ОĐŋОвĐĩŅ‰ĐĩĐŊиŅ SMS-ŅĐžĐžĐąŅ‰ĐĩĐŊиК + ОĐŋОвĐĩŅ‰ĐĩĐŊиŅ + Xiaomi Smart Band 7 Pro + Xiaomi Watch S1 Active + Mi Watch Color Sports + Pixoo + ПĐĩŅ€ĐĩдаваŅ‚ŅŒ ŅƒĐ˛ĐĩĐ´ĐžĐŧĐģĐĩĐŊиŅ ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊиК ĐŊĐ° ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚вО + ПĐĩŅ€ĐĩдаваŅ‚ŅŒ ŅƒĐ˛ĐĩĐ´ĐžĐŧĐģĐĩĐŊиŅ + ĐĸŅ€ĐĩĐšĐģŅ€Đ°ĐŊĐŊиĐŊĐŗ + МаĐēĐĩŅ‚ видĐļĐĩŅ‚Đ° + На ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚вĐĩ ĐŊĐĩ ĐžŅŅ‚Đ°ĐģĐžŅŅŒ ŅĐ˛ĐžĐąĐžĐ´ĐŊŅ‹Ņ… ĐŧĐĩŅŅ‚ ĐŋОд ŅĐēŅ€Đ°ĐŊŅ‹ видĐļĐĩŅ‚Ов (вŅĐĩĐŗĐž: %1$s) + НĐĩ ĐąĐĩŅĐŋĐžĐēОиŅ‚ŅŒ - ВĐēĐģ. + БĐĩССвŅƒŅ‡ĐŊŅ‹Đš Ņ€ĐĩĐļиĐŧ Ņ‚ĐĩĐģĐĩŅ„ĐžĐŊĐ° + НĐĩиСвĐĩŅŅ‚ĐŊŅ‹Đš Ņ‚иĐŋ Ņ‚Ņ€ĐĩĐŊиŅ€ĐžĐ˛Đēи - %s + ĐĸиĐŋ ĐŊĐžŅˆĐĩĐŊиŅ + Pebble (ĐŊĐ° ОйŅƒĐ˛Đ¸) + ПĐĩŅ€ĐĩĐŧĐĩŅŅ‚иŅ‚ŅŒ вŅ‹ŅˆĐĩ + ВŅ‹ Ņ‚ĐžŅ‡ĐŊĐž Ņ…ĐžŅ‚иŅ‚Đĩ ŅƒĐ´Đ°ĐģиŅ‚ŅŒ ÂĢ%1$sÂģ? + НоŅ€ĐŧĐ°ĐģŅŒĐŊŅ‹Đš / На вийŅ€Đ°Ņ†Đ¸Đ¸ + ДоĐŋĐžĐģĐŊиŅ‚ĐĩĐģŅŒĐŊĐ°Ņ Ņ†ĐĩĐģŅŒ + ОŅ‚ĐŋŅ€Đ°Đ˛ĐģŅŅ‚ŅŒ ŅƒĐ˛ĐĩĐ´ĐžĐŧĐģĐĩĐŊиĐĩ, ĐēĐžĐŗĐ´Đ° ŅƒŅ€ĐžĐ˛ĐĩĐŊŅŒ ĐļиСĐŊĐĩĐŊĐŊОК ŅĐŊĐĩŅ€Đŗии Са ĐŋĐžŅĐģĐĩĐ´ĐŊиĐĩ 7 Đ´ĐŊĐĩĐš Đ´ĐžŅŅ‚иĐŗĐ°ĐĩŅ‚ 30, 60 иĐģи 100 + ГоŅ€ĐŊŅ‹Đš ĐŋĐžŅ…Од + ĐŖŅ€ĐžĐ˛ĐĩĐŊŅŒ ĐļиСĐŊĐĩĐŊĐŊОК ŅĐŊĐĩŅ€Đŗии + 2 видĐļĐĩŅ‚Đ° + ĐŖĐ´Đ°ĐģиŅ‚ŅŒ ŅĐēŅ€Đ°ĐŊ видĐļĐĩŅ‚Đ° + НĐĩ ĐąĐĩŅĐŋĐžĐēОиŅ‚ŅŒ - ĐĸĐžĐģŅŒĐēĐž ĐąŅƒĐ´Đ¸ĐģŅŒĐŊиĐē + 1 ĐŊавĐĩŅ€Ņ…Ņƒ, 2 вĐŊиСŅƒ + 2 ĐŊавĐĩŅ€Ņ…Ņƒ, 2 вĐŊиСŅƒ + ДоĐģĐļĐŊĐž ĐąŅ‹Ņ‚ŅŒ ĐŧиĐŊиĐŧŅƒĐŧ %1$s ŅĐēŅ€Đ°ĐŊОв + На вийŅ€Đ°Ņ†Đ¸Đ¸ / БĐĩССвŅƒŅ‡ĐŊŅ‹Đš + ВидĐļĐĩŅ‚ + ОŅ‚ĐŋŅ€Đ°Đ˛ĐģŅŅ‚ŅŒ ŅƒĐ˛ĐĩĐ´ĐžĐŧĐģĐĩĐŊиĐĩ ĐŋŅ€Đ¸ Đ´ĐžŅŅ‚иĐļĐĩĐŊии ĐŧĐ°ĐēŅĐ¸ĐŧĐ°ĐģŅŒĐŊĐžĐŗĐž ŅƒŅ€ĐžĐ˛ĐŊŅ ĐļиС. ŅĐŊĐĩŅ€Đŗии Са Đ´ĐĩĐŊŅŒ + Redmi Smart Band 2 + Đ­ĐēŅ€Đ°ĐŊ видĐļĐĩŅ‚Đ° + ПĐĩŅ€ĐĩĐŧĐĩŅŅ‚иŅ‚ŅŒ ĐŊиĐļĐĩ + 1 видĐļĐĩŅ‚ + НĐĩ ĐąĐĩŅĐŋĐžĐēОиŅ‚ŅŒ - ĐĸĐžĐģŅŒĐēĐž ваĐļĐŊŅ‹Đĩ + НĐĩ СадаĐŊĐž + ПодŅ‚иĐŋ видĐļĐĩŅ‚Đ° + ВŅ€ĐĩĐŧŅ ŅŅ‚ĐžŅ + ВŅ€ĐĩĐŧŅ Đ°ĐēŅ‚ивĐŊĐžŅŅ‚и + НаŅ€ŅƒŅ‡ĐŊŅ‹Đš ĐąŅ€Đ°ŅĐģĐĩŅ‚ + Đ­ĐēŅ€Đ°ĐŊ %s + ВŅ‹ĐąĐĩŅ€Đ¸Ņ‚Đĩ вŅĐĩ видĐļĐĩŅ‚Ņ‹ + 2 ĐŊавĐĩŅ€Ņ…Ņƒ, 1 вĐŊиСŅƒ + КоĐģŅŒĐĩ (ŅˆĐŊŅƒŅ€ĐžĐē ĐŊĐ° ŅˆĐĩĐĩ) + ПŅ€ĐžĐŗŅ€ĐĩŅŅ ŅĐĩĐŗОдĐŊŅ + ПŅ€ĐžĐŗŅ€ĐĩŅŅ Са 7 Đ´ĐŊĐĩĐš + НĐĩ ĐąĐĩŅĐŋĐžĐēОиŅ‚ŅŒ - ВŅ‹ĐēĐģ. + НоŅ€ĐŧĐ°ĐģŅŒĐŊŅ‹Đš / БĐĩССвŅƒŅ‡ĐŊŅ‹Đš + БоŅ€ŅŒĐąĐ° + ColaCao 2023 + ColaCao 2021 + АĐēŅ‚ивиŅ€ĐžĐ˛Đ°Ņ‚ŅŒ ŅĐēŅ€Đ°ĐŊ Ņ…ĐģĐžĐŋĐēĐžĐŧ Ņ€ŅƒĐēĐ°Đŧи" + ПовŅ‚ĐžŅ€ĐŊŅ‹Đš Ņ…ĐģĐžĐŋĐžĐē ĐŋĐžĐŗĐ°ŅĐ¸Ņ‚ ĐĩĐŗĐž" + Đ­ĐēŅ€Đ°ĐŊ ĐŋĐžĐŗĐ°ŅĐŊĐĩŅ‚ ĐŋĐžŅĐģĐĩ ĐŋŅ€ĐžĐ´ĐžĐģĐļиŅ‚ĐĩĐģŅŒĐŊОК Ņ‚иŅˆĐ¸ĐŊŅ‹, СаĐŧĐĩŅ‡ĐĩĐŊĐŊОК ĐŧиĐēŅ€ĐžŅ„ĐžĐŊĐžĐŧ + Mijia Temperature and Humidity Sensor 2 + ĐŖĐēаСаĐŊиŅ ĐŋĐž ĐŊавиĐŗĐ°Ņ†Đ¸Đ¸ + НаŅŅ‚Ņ€ĐžĐšĐēи вŅŅ‚Ņ€ĐžĐĩĐŊĐŊĐžĐŗĐž ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊиŅ ĐŊавиĐŗĐ°Ņ†Đ¸Đ¸ + ДоĐģĐļĐŊĐž Đģи ĐŋĐžŅĐ˛ĐģŅŅ‚ŅŒŅŅ ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊиĐĩ ĐŊавиĐŗĐ°Ņ†Đ¸Đ¸ ĐŋŅ€Đ¸ ĐŋĐžĐģŅƒŅ‡ĐĩĐŊии ĐŊавиĐŗĐ°Ņ†Đ¸ĐžĐŊĐŊŅ‹Ņ… ŅƒĐēаСаĐŊиК + ВибŅ€Đ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ ĐŋŅ€Đ¸ ĐŊОвŅ‹Ņ… ŅƒĐēаСаĐŊиŅŅ… + АвŅ‚ĐžĐŋĐžŅĐ˛ĐģĐĩĐŊиĐĩ + ИĐŧŅ ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚ва + ДоĐģĐļĐŊŅ‹ Đģи Ņ‡Đ°ŅŅ‹ вийŅ€Đ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ ĐŋŅ€Đ¸ ĐŋĐžĐģŅƒŅ‡ĐĩĐŊии иĐģи иСĐŧĐĩĐŊĐĩĐŊии ĐŊавиĐŗĐ°Ņ†Đ¸ĐžĐŊĐŊŅ‹Ņ… ŅƒĐēаСаĐŊиК (ĐēĐžĐŗĐ´Đ° ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊиĐĩ ĐŋĐžĐēаСаĐŊĐž) + Redmi Watch 2 Lite + ĐĄĐžŅŅ‚ĐžŅĐŊиĐĩ ŅĐžĐĩдиĐŊĐĩĐŊиŅ + Redmi Smart Band Pro + Nothing Ear (2) + Nothing Ear (Stick) + ЛŅ‘ĐŗĐēĐžĐĩ Đ°ĐēŅ‚ивĐŊĐžĐĩ ĐŋОдавĐģĐĩĐŊиĐĩ ŅˆŅƒĐŧĐ° + ПŅ€ĐžĐˇŅ€Đ°Ņ‡ĐŊĐžŅŅ‚ŅŒ + Honor Band 5 + Huawei Watch GT + Huawei Band 4 Pro + Huawei Watch GT 2 Pro + Huawei Watch GT 2e + Huawei Talk Band B6 + Huawei Watch GT 3 Pro + ĐŖвĐĩĐ´ĐžĐŧĐģĐĩĐŊиĐĩ ĐŊĐ° ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚вĐĩ ĐŋŅ€Đ¸ ĐžŅ‚ŅĐžĐĩдиĐŊĐĩĐŊии ĐžŅ‚ Bluetooth. + ĐĸĐžĐģŅŒĐēĐž ĐĩŅĐģи вĐēĐģŅŽŅ‡ĐĩĐŊĐ° Đ°ĐēŅ‚иваŅ†Đ¸Ņ ĐŋĐž ĐŋОдĐŊŅŅ‚иŅŽ Ņ€ŅƒĐēи + \"НĐĩ ĐąĐĩŅĐŋĐžĐēОиŅ‚ŅŒ\", ĐēĐžĐŗĐ´Đ° ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚вО ŅĐŊŅŅ‚Đž + ВŅ€ŅƒŅ‡ĐŊŅƒŅŽ + Honor Band 4 + Honor Band 6 + Honor Band 7 + Huawei Band (AW70) + ВĐĩŅŅŒ Đ´ĐĩĐŊŅŒ + Honor Band 3 + Huawei Band 8 + Huawei Band 6 + Huawei Band 7 + Đ ĐĩĐļиĐŧ Ņ€Đ°ĐąĐžŅ‚Ņ‹ + НĐĩ ĐžŅ‚ĐēĐģŅŽŅ‡Đ°Ņ‚ŅŒ \"ŅƒĐŧĐŊŅ‹Đš ĐŋОдŅŠŅ‘Đŧ\". + НĐĩ вĐēĐģŅŽŅ‡Đ°Ņ‚ŅŒ \"ŅƒĐŧĐŊŅ‹Đš ĐŋОдŅŠŅ‘Đŧ\". + HUAWEI TruSleep â„ĸ + ОŅ‚ŅĐģĐĩĐļиваĐŊиĐĩ ĐēĐ°Ņ‡ĐĩŅŅ‚ва ŅĐŊĐ° и Ņ…Đ°Ņ€Đ°ĐēŅ‚ĐĩŅ€Đ° Đ´Ņ‹Ņ…Đ°ĐŊиŅ в Ņ€ĐĩĐ°ĐģŅŒĐŊĐžĐŧ вŅ€ĐĩĐŧĐĩĐŊи. +\nАĐŊĐ°ĐģиС СаĐēĐžĐŊĐžĐŧĐĩŅ€ĐŊĐžŅŅ‚ĐĩĐš ŅĐŊĐ° и Ņ‚ĐžŅ‡ĐŊŅ‹Đš диаĐŗĐŊОС 6 Ņ‚иĐŋОв ĐŋŅ€ĐžĐąĐģĐĩĐŧ ŅĐž ŅĐŊĐžĐŧ. + ПĐĩŅ€ĐĩĐŋОдĐēĐģŅŽŅ‡Đ°Ņ‚ŅŒŅŅ Ņ‚ĐžĐģŅŒĐēĐž Đē ĐŋОдĐēĐģŅŽŅ‡ĐĩĐŊĐŊŅ‹Đŧ ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚ваĐŧ + Ņ€Đ°ŅĐŋОСĐŊаваŅ‚ŅŒ ĐĩСдŅƒ ĐŊĐ° вĐĩĐģĐžŅĐ¸ĐŋĐĩĐ´Đĩ + Ņ€Đ°ŅĐŋОСĐŊаваŅ‚ŅŒ Ņ…ОдŅŒĐąŅƒ + ŅĐŋŅ€Đ°ŅˆĐ¸Đ˛Đ°Ņ‚ŅŒ + авŅ‚ĐžĐŧĐ°Ņ‚иŅ‡ĐĩŅĐēи + ПŅ€Đ¸ĐŊŅŅ‚иĐĩ Ņ‚ĐĩĐģ. вŅ‹ĐˇĐžĐ˛ĐžĐ˛ + ВĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒ ĐŋŅ€Đ¸ĐŊŅŅ‚иĐĩ вŅ‹ĐˇĐžĐ˛ĐžĐ˛ Ņ ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚ва + ОŅ‚ĐēĐģĐžĐŊĐĩĐŊиĐĩ Ņ‚ĐĩĐģ. вŅ‹ĐˇĐžĐ˛ĐžĐ˛ + АвŅ‚ОиСĐŧĐĩŅ€ĐĩĐŊиĐĩ ŅĐĩŅ€Đ´Ņ†ĐĩйиĐĩĐŊиŅ + АвŅ‚ОиСĐŧĐĩŅ€ĐĩĐŊиĐĩ ĐēиŅĐģĐžŅ€ĐžĐ´Đ° в ĐēŅ€ĐžĐ˛Đ¸ + ФОŅ€ŅĐ¸Ņ€ĐžĐ˛Đ°ĐŊиĐĩ ĐžĐŋŅ†Đ¸Đš + ФОŅ€ŅĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ ŅƒĐŧĐŊŅ‹Đš ĐąŅƒĐ´Đ¸ĐģŅŒĐŊиĐē + ФОŅ€ŅĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ ĐŋОддĐĩŅ€ĐļĐēŅƒ ŅƒĐŧĐŊŅ‹Ņ… ĐąŅƒĐ´Đ¸ĐģŅŒĐŊиĐēОв. +\nИСПОЛĐŦЗĐŖЙĐĸЕ НА СВОЙ РИСК + ФОŅ€ŅĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ ĐŧĐĩŅŅ‚Đž ĐŊĐžŅˆĐĩĐŊиŅ + ФОŅ€ŅĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ ĐŋОддĐĩŅ€ĐļĐēŅƒ Ņ€Đ°ŅĐŋĐžĐģĐžĐļĐĩĐŊиŅ ĐŊĐžŅˆĐĩĐŊиŅ ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚ва. +\nИСПОЛĐŦЗĐŖЙĐĸЕ НА СВОЙ РИСК + ФОŅ€ŅĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ ĐŋОддĐĩŅ€ĐļĐēŅƒ \"ĐŊĐĩ ĐąĐĩŅĐŋĐžĐēОиŅ‚ŅŒ\" + ИĐŗĐŊĐžŅ€Đ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ ŅŅ‚Đ°Ņ‚ŅƒŅ ĐŊĐ°Ņ‡Đ°ĐģĐ° вŅŅ‚аваĐŊиŅ + МоĐļĐĩŅ‚ ĐŋĐžĐŧĐžŅ‡ŅŒ иŅĐŋŅ€Đ°Đ˛Đ¸Ņ‚ŅŒ Ņ€Đ°ŅĐŋОСĐŊĐ°ĐŊиĐĩ ŅĐŊĐ°. ĐĄŅ€Đ°ĐˇŅƒ видиĐŧĐž в ОйСОŅ€Đĩ Đ´ĐŊĐĩвĐŊŅ‹Ņ… Đ°ĐēŅ‚ивĐŊĐžŅŅ‚ĐĩĐš. + ИĐŗĐŊĐžŅ€Đ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ ŅŅ‚Đ°Ņ‚ŅƒŅ ĐžĐēĐžĐŊŅ‡Đ°ĐŊиŅ вŅŅ‚аваĐŊиŅ + ПовŅ‚ĐžŅ€ĐŊŅ‹Đš Đ°ĐŊĐ°ĐģиС Đ´Đ°ĐŊĐŊŅ‹Ņ… Ņ‚Ņ€ĐĩĐŊиŅ€ĐžĐ˛Đēи + ОŅ‚ĐŋŅ€Đ°Đ˛Đ¸Ņ‚ŅŒ СаĐŋŅ€ĐžŅ ĐžŅ‚ĐģĐ°Đ´Đēи ĐŊĐ° ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚вО Huawei + ЗаĐŋŅ€ĐžŅ ĐžŅ‚ĐģĐ°Đ´Đēи + ĐŖĐģŅƒŅ‡ŅˆĐĩĐŊĐŊĐžĐĩ ĐžŅ‚ŅĐģĐĩĐļиваĐŊиĐĩ ŅĐŊĐ° + ĐŊĐĩŅ‚ + НаŅŅ‚Ņ€ĐžĐšĐēи Ņ€Đ°ŅĐŋОСĐŊĐ°ĐŊиŅ Đ°ĐēŅ‚ивĐŊĐžŅŅ‚и + Ņ€Đ°ŅĐŋОСĐŊаваŅ‚ŅŒ ĐąĐĩĐŗ + Ņ€Đ°ŅĐŋОСĐŊаваŅ‚ŅŒ ĐŗŅ€ĐĩĐąĐģŅŽ + ВĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒ ĐžŅ‚ĐēĐģĐžĐŊĐĩĐŊиĐĩ вŅ‹ĐˇĐžĐ˛ĐžĐ˛ Ņ ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚ва + ЗаĐŋŅ€ĐĩŅ‚иŅ‚ŅŒ \"ĐŊĐ°ĐšŅ‚и ĐŧОК Ņ‚ĐĩĐģĐĩŅ„ĐžĐŊ\" ĐŋŅ€Đ¸ вĐēĐģŅŽŅ‡ĐĩĐŊĐŊĐžĐŧ Ņ€ĐĩĐļиĐŧĐĩ \"ĐŊĐĩ ĐąĐĩŅĐŋĐžĐēОиŅ‚ŅŒ\" + НĐĩĐēĐžŅ‚ĐžŅ€Ņ‹Đĩ ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚ва ĐģĐžĐļĐŊĐž ĐŋĐžĐŧĐĩŅ‡Đ°ŅŽŅ‚ ŅĐĩĐąŅ ĐēĐ°Đē ĐŊĐĩ иĐŧĐĩŅŽŅ‰Đ¸Đĩ ĐŋОддĐĩŅ€ĐļĐēи ŅŅ‚иŅ… ĐžĐŋŅ†Đ¸Đš. В ŅŅ‚иŅ… ĐŊĐ°ŅŅ‚Ņ€ĐžĐšĐēĐ°Ņ… иŅ… ĐŧĐžĐļĐŊĐž ĐŋĐžĐŋŅ‹Ņ‚Đ°Ņ‚ŅŒŅŅ вĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒ. +\nИСПОЛĐŦЗĐŖЙĐĸЕ НА СВОЙ РИСК +\nЗа ĐŋĐžĐŧĐžŅ‰ŅŒŅŽ ОйŅ€Đ°Ņ‰Đ°ĐšŅ‚ĐĩŅŅŒ Đē ВиĐēи + ФОŅ€ŅĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ ĐŋОддĐĩŅ€ĐļĐēŅƒ Ņ€ĐĩĐļиĐŧĐ° \"ĐŊĐĩ ĐąĐĩŅĐŋĐžĐēОиŅ‚ŅŒ\". +\nИСПОЛĐŦЗĐŖЙĐĸЕ НА СВОЙ РИСК + МоĐļĐĩŅ‚ ĐŋĐžĐŧĐžŅ‡ŅŒ иŅĐŋŅ€Đ°Đ˛Đ¸Ņ‚ŅŒ Ņ€Đ°ŅĐŋОСĐŊĐ°ĐŊиĐĩ ŅĐŊĐ°. ĐĄŅ€Đ°ĐˇŅƒ видиĐŧĐž в ОйСОŅ€Đĩ Đ´ĐŊĐĩвĐŊŅ‹Ņ… Đ°ĐēŅ‚ивĐŊĐžŅŅ‚ĐĩĐš. + Đ­Ņ‚Đ° ĐžĐŋŅ†Đ¸Ņ ĐŋОвĐģиŅĐĩŅ‚ ĐŊĐ° Ņ‡Ņ‚Đž-Ņ‚Đž ĐŋĐžŅĐģĐĩ ĐŊĐĩĐēĐžŅ‚ĐžŅ€Ņ‹Ņ… ОйĐŊОвĐģĐĩĐŊиК + ПĐĩŅ€ĐĩĐŋОдĐēĐģŅŽŅ‡Đ°Ņ‚ŅŒŅŅ Ņ‚ĐžĐģŅŒĐēĐž Đē ĐŋОдĐēĐģŅŽŅ‡ĐĩĐŊĐŊŅ‹Đŧ ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚ваĐŧ, Đ° ĐŊĐĩ ĐēĐž вŅĐĩĐŧ ĐŋОдŅ€ŅĐ´ + ЗаŅ€ŅĐ´ йаŅ‚Đ°Ņ€Đĩи ŅĐģиŅˆĐēĐžĐŧ ĐŊиСОĐē + МоŅ‰ĐŊĐžŅŅ‚ŅŒ СвŅƒĐēОиСĐģŅƒŅ‡Đ°Ņ‚ĐĩĐģŅ + ПаŅ€ĐžĐģŅŒ Đ´ĐžĐģĐļĐĩĐŊ ŅĐžŅŅ‚ĐžŅŅ‚ŅŒ иС 4 Ņ†Đ¸Ņ„Ņ€ + Xiaomi Watch S1 Pro + ĐŖŅŅ‚Đ°ĐŊОвĐēĐ° Ņ†Đ¸Ņ„ĐĩŅ€ĐąĐģĐ°Ņ‚Đ° СавĐĩŅ€ŅˆĐĩĐŊĐ° + ĐŖŅŅ‚Đ°ĐŊОвĐēĐ° Ņ†Đ¸Ņ„ĐĩŅ€ĐąĐģĐ°Ņ‚Đ° ĐŊĐĩ ŅƒĐ´Đ°ĐģĐ°ŅŅŒ + ПŅ€Đ¸ĐŊŅƒĐ´Đ¸Ņ‚ĐĩĐģŅŒĐŊĐ°Ņ ŅƒŅŅ‚Đ°ĐŊОвĐēĐ° Ņ‚иĐŋĐ° ŅĐžĐĩдиĐŊĐĩĐŊиŅ + АвŅ‚ĐžĐŧĐ°Ņ‚иŅ‡ĐĩŅĐēиК + Bluetooth LE (ĐŊиСĐēĐžĐĩ ŅĐŊĐĩŅ€ĐŗĐžĐŋĐžŅ‚Ņ€ĐĩĐąĐģĐĩĐŊиĐĩ) + Bluetooth Classic + ИĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸Ņ Ой Đ°ĐēŅ‚ивĐŊĐžŅŅ‚и + ВĐĩŅ€ŅĐ¸Ņ 1 + ВĐĩŅ€ŅĐ¸Ņ 3 + ВĐĩŅ€ŅĐ¸Ņ ĐŋŅ€ĐžŅ‚ĐžĐēĐžĐģĐ° + ВĐĩŅ€ŅĐ¸Ņ 2 + ПĐģĐžŅĐēĐžĐĩ Ņ€Đ°ŅŅŅ‚ĐžŅĐŊиĐĩ + Xiaomi Watch S1 + Xiaomi Watch S3 + ЗаĐŗŅ€ŅƒĐˇĐēĐ° Ņ†Đ¸Ņ„ĐĩŅ€ĐąĐģĐ°Ņ‚Đ°â€Ļ + ЗаĐŗŅ€ŅƒĐˇĐēĐ° Ņ†Đ¸Ņ„ĐĩŅ€ĐąĐģĐ°Ņ‚Đ° + ВозĐŧĐžĐļĐŊĐž, ваĐŧ ŅƒĐ´Đ°Ņ‚ŅŒŅŅ СаŅŅ‚авиŅ‚ŅŒ ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚вО Ņ€Đ°ĐąĐžŅ‚Đ°Ņ‚ŅŒ Ņ Gadgetbridge, ĐŋŅ€Đ¸ĐŊŅƒĐ´Đ¸Ņ‚ĐĩĐģŅŒĐŊĐž вŅ‹ŅŅ‚авив ĐŊŅƒĐļĐŊŅ‹Đš Ņ‚иĐŋ ŅĐžĐĩдиĐŊĐĩĐŊиŅ + Xiaomi Smart Band 8 Pro + Mijia MHO-C303 + Sony WI-SP600N + 155 ŅƒĐ´/Đŧ + 165 ŅƒĐ´/Đŧ + ПоŅ€ĐžĐŗ ĐŋŅ€ĐĩĐ´ŅƒĐŋŅ€ĐĩĐļĐ´ĐĩĐŊиŅ Đž ĐŋОвŅ‹ŅˆĐĩĐŊĐŊĐžĐŧ ŅĐĩŅ€Đ´Ņ†ĐĩйиĐĩĐŊии + ФиŅ‚ĐŊĐĩŅ-ŅƒĐŋŅ€Đ°ĐļĐŊĐĩĐŊиŅ + ФиСиŅ‡ĐĩŅĐēĐ°Ņ ĐŋОдĐŗĐžŅ‚ОвĐēĐ° + ĐĸŅ…ŅĐēвОĐŊĐ´Đž + ФĐĩŅ…Ņ‚ОваĐŊиĐĩ + КŅĐŊĐ´Đž + ПодŅŠŅ‘Đŧ Ņ‚ŅƒĐģОвиŅ‰Đ° + ПодŅ‚ŅĐŗиваĐŊиŅ + ПĐģĐ°ĐŊĐēĐ° + КоĐŋŅŒĐĩ + ПŅ€Ņ‹ĐļĐēи в Đ´ĐģиĐŊŅƒ + ĐĸŅ€Đ°ĐŧĐŋĐģиĐŊ + БаĐģĐĩŅ‚ + БоĐĩвŅ‹Đĩ иŅĐēŅƒŅŅŅ‚ва + ĐĸĐ°Đš-Ņ‡Đ¸ + 175 ŅƒĐ´/Đŧ + 185 ŅƒĐ´/Đŧ + 195 ŅƒĐ´/Đŧ + 205 ŅƒĐ´/Đŧ + ВŅ‹ĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒ ĐŊĐ°ĐŋĐžĐŧиĐŊĐ°ĐŊиŅ Ой ŅƒĐŋĐžŅ‚Ņ€ĐĩĐąĐģĐĩĐŊии вОдŅ‹ ĐŊĐ° вŅ€ĐĩĐŧĐĩĐŊĐŊОК ĐŋŅ€ĐžĐŧĐĩĐļŅƒŅ‚ĐžĐē + БĐĩĐŗ в ĐŋĐžĐŧĐĩŅ‰ĐĩĐŊии + КаŅ€Đ°Ņ‚Đĩ + ОŅ‚ĐļиĐŧĐ°ĐŊиŅ + ПŅ€Ņ‹ĐļĐēи в вŅ‹ŅĐžŅ‚Ņƒ + ОбŅ€ŅƒŅ‡ + CMF Watch Pro + ОŅ…ĐžŅ‚Đ° + Đ Ņ‹ĐąĐ°ĐģĐēĐ° + Redmi Watch 2 + КаŅ€Ņ‚иĐŊĐŗ + БиĐģŅŒŅŅ€Đ´ + ВŅ‹ŅˆĐ¸ĐąĐ°ĐģŅ‹ + ĐĄĐžŅ„Ņ‚йОĐģ + ПиĐēĐģйОĐģ \ No newline at end of file diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index a6b3daec9..495d44502 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -1,2 +1,4 @@ - \ No newline at end of file + + āš€ā¸ā¸ĩāšˆā¸ĸā¸§ā¸ā¸ąā¸š Gadgetbridge + \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 130456033..9b8622d3a 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -900,7 +900,7 @@ Lemfo SG2 Amazfit T-Rex Mi Band 5 - swolf Ņ–ĐŊĐ´ĐĩĐēŅ + SWOLF ĐĄŅ‚иĐģŅŒ ĐŋĐģаваĐŊĐŊŅ ЗаŅŅ‚ĐžŅĐžĐ˛ŅƒŅ”Ņ‚ŅŒŅŅ Đ´ĐģŅ ĐŋĐžŅŅ‚Đ°Ņ‡Đ°ĐģŅŒĐŊиĐēĐ° ĐŋĐžĐŗОди LineageOS, Ņ–ĐŊŅˆŅ– вĐĩŅ€ŅŅ–Ņ— Android ĐŧĐ°ŅŽŅ‚ŅŒ виĐēĐžŅ€Đ¸ŅŅ‚ОвŅƒĐ˛Đ°Ņ‚и СаŅŅ‚ĐžŅŅƒĐŊĐēи ŅĐē-ĐžŅ‚ \"Weather notification\". ШŅƒĐēĐ°ĐšŅ‚Đĩ ĐąŅ–ĐģŅŒŅˆĐĩ вŅ–Đ´ĐžĐŧĐžŅŅ‚ĐĩĐš Ņƒ вŅ–ĐēŅ– Gadgetbridge. ВиĐēĐžŅ€Đ¸ŅŅ‚ОвŅƒĐ˛Đ°Ņ‚и ĐŋОдŅ–Ņ— ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ Đ´ĐģŅ СаĐŋŅƒŅĐēŅƒ Đ´Ņ–Đš Ņ– Ņ‚Ņ€Đ°ĐŊŅĐģŅŅ†Ņ–Đš Android @@ -1328,7 +1328,7 @@ ЧаŅ ŅĐŊŅƒ FitPro ЗвŅƒĐēОвиК ŅĐ¸ĐŗĐŊĐ°Đģ - НŅ–Ņ‡ĐžĐŗĐž ĐŋŅ€ĐžŅĐģŅƒŅ…ОвŅƒĐ˛Đ°Ņ‚и (1) + Nothing Ear (1) ВиŅĐ˛ĐģĐĩĐŊĐŊŅ ОдŅĐŗĐ°ĐŊĐŊŅ ĐŊĐ° вŅƒŅ…Đ° ВŅ–Đ´Ņ‚вОŅ€ĐĩĐŊĐŊŅ/СŅƒĐŋиĐŊĐēĐ° ĐŧŅƒĐˇĐ¸Đēи СаĐģĐĩĐļĐŊĐž вŅ–Đ´ Ņ‚ĐžĐŗĐž, Ņ‡Đ¸ ОдŅĐŗĐŊĐĩĐŊĐž ĐŊавŅƒŅˆĐŊиĐēи АŅƒĐ´Ņ–ĐžŅ€ĐĩĐļиĐŧ @@ -1359,7 +1359,7 @@ ĐĄĐĩĐŊŅĐžŅ€ĐŊĐĩ ĐąĐģĐžĐēŅƒĐ˛Đ°ĐŊĐŊŅ ВиĐŧĐēĐŊŅƒŅ‚и ĐŋОдŅ–Ņ— Ņ‚ĐžŅ€ĐēĐ°ĐŊĐŊŅ ЕĐēŅĐŋĐĩŅ€Đ¸ĐŧĐĩĐŊŅ‚Đ°ĐģŅŒĐŊĐĩ - ГŅƒŅ‡ĐŊŅ–ŅŅ‚ŅŒ дОвĐēŅ–ĐģĐģŅ + ГŅƒŅ‡ĐŊŅ–ŅŅ‚ŅŒ ŅĐĩŅ€ĐĩдОвиŅ‰Đ° ФОĐēŅƒŅ ĐŗĐžĐģĐžŅŅƒ ПŅ–Đ´ŅĐ¸ĐģĐĩĐŊĐŊŅ ĐŗĐžĐģĐžŅŅƒ ЛŅ–виК @@ -1379,7 +1379,7 @@ КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊавĐēĐžĐģиŅˆĐŊŅ–Đŧ СвŅƒĐēĐžĐŧ ЗĐŊиĐļĐĩĐŊĐŊŅ ŅˆŅƒĐŧŅƒ вŅ–Ņ‚Ņ€Ņƒ ЗĐŊиĐļĐĩĐŊĐŊŅ Ņ‚иŅĐēŅƒ Ņ‡ĐĩŅ€ĐĩС ĐŊавĐēĐžĐģиŅˆĐŊŅ–Đš ŅˆŅƒĐŧ - ЗаĐŋОйŅ–ĐŗĐ°ĐŊĐŊŅ вŅ–Đ´Ņ‡ŅƒŅ‚Ņ‚ŅŽ Ņ‚иŅĐēŅƒ Ņƒ вŅƒŅ…Đ°Ņ… Са ĐŊĐĩвиĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐŊŅ Đ°ĐēŅ‚ивĐŊĐžĐŗĐž ŅˆŅƒĐŧĐžĐŋŅ€Đ¸Đ´ŅƒŅˆĐĩĐŊĐŊŅ + ЗаĐŋОйŅ–ĐŗĐ°ĐŊĐŊŅ вŅ–Đ´Ņ‡ŅƒŅ‚Ņ‚ŅŽ Ņ‚иŅĐēŅƒ Ņƒ вŅƒŅ…Đ°Ņ…, ĐēĐžĐģи ĐŊĐĩ виĐēĐžŅ€Đ¸ŅŅ‚ОвŅƒŅ”Ņ‚ŅŒŅŅ Đ°ĐēŅ‚ивĐŊĐĩ ŅˆŅƒĐŧĐžĐŋĐžĐŗĐģиĐŊĐ°ĐŊĐŊŅ ЗоŅĐĩŅ€ĐĩдиŅ‚иŅŅ ĐŊĐ° ĐŗĐžĐģĐžŅŅ– ВиĐŧĐē. ĐĄĐŋĐĩŅ€ĐĩĐ´Ņƒ @@ -1408,9 +1408,9 @@ ЗвиŅ‡Đ°ĐšĐŊŅ– Galaxy Buds Live Sony WH-1000XM3 - АĐēŅ‚ивĐŊĐĩ ŅˆŅƒĐŧОСаĐŗĐģŅƒŅˆĐĩĐŊĐŊŅ + АĐēŅ‚ивĐŊĐĩ ŅˆŅƒĐŧĐžĐŋĐžĐŗĐģиĐŊĐ°ĐŊĐŊŅ Đ ĐĩĐļиĐŧ - ШŅƒĐŧОСаĐŗĐģŅƒŅˆĐĩĐŊĐŊŅ + ШŅƒĐŧĐžĐŋĐžĐŗĐģиĐŊĐ°ĐŊĐŊŅ НавĐēĐžĐģиŅˆĐŊŅ–Đš СвŅƒĐē Đ Ņ–вĐĩĐŊŅŒ ĐŊавĐēĐžĐģиŅˆĐŊŅ–Ņ… СвŅƒĐēŅ–в ĐĄĐŋĐĩŅ€ĐĩĐ´Ņƒ ĐŋŅ€Đ°Đ˛ĐžŅ€ŅƒŅ‡ @@ -1560,13 +1560,13 @@ НĐĩвŅ–Đ´ĐžĐŧĐž РОСĐŋĐžŅ‡Đ°Ņ‚и ОĐŋŅ‚иĐŧŅ–СŅƒĐ˛Đ°Ņ‚и - ОĐŋŅ‚иĐŧŅ–СаŅ‚ĐžŅ€ ĐŋŅ€Đ¸ĐŗĐŊŅ–Ņ‡ĐĩĐŊĐŊŅ ŅˆŅƒĐŧŅƒ - НаŅ‚иŅĐŊŅ–Ņ‚ŅŒ, Ņ‰ĐžĐą СаĐŋŅƒŅŅ‚иŅ‚и ĐžĐŋŅ‚иĐŧŅ–СаŅ‚ĐžŅ€ ĐŋŅ€Đ¸ĐŗĐŊŅ–Ņ‡ĐĩĐŊĐŊŅ ŅˆŅƒĐŧŅƒ. + ОĐŋŅ‚иĐŧŅ–СаŅ‚ĐžŅ€ ŅˆŅƒĐŧĐžĐŋĐžĐŗĐģиĐŊĐ°ĐŊĐŊŅ + НаŅ‚иŅĐŊŅ–Ņ‚ŅŒ, Ņ‰ĐžĐą СаĐŋŅƒŅŅ‚иŅ‚и ĐžĐŋŅ‚иĐŧŅ–СаŅ‚ĐžŅ€ ŅˆŅƒĐŧĐžĐŋĐžĐŗĐģиĐŊĐ°ĐŊĐŊŅ. АŅ‚ĐŧĐžŅŅ„ĐĩŅ€ĐŊиК Ņ‚иŅĐē ЗаĐŋŅƒŅĐēâ€Ļ НĐĩ СаĐŋŅƒŅ‰ĐĩĐŊĐž АĐŊĐ°ĐģŅ–СŅƒĐ˛Đ°ĐŊĐŊŅâ€Ļ - ОĐŋŅ‚иĐŧŅ–СаŅ‚ĐžŅ€ ĐŋŅ€Đ¸ĐŗĐŊŅ–Ņ‡ĐĩĐŊĐŊŅ ŅˆŅƒĐŧŅƒ + ОĐŋŅ‚иĐŧŅ–СаŅ‚ĐžŅ€ ŅˆŅƒĐŧĐžĐŋĐžĐŗĐģиĐŊĐ°ĐŊĐŊŅ ВиĐŧŅ–Ņ€ŅŽĐ˛Đ°ĐŊĐŊŅ ŅŅ‚Đ°ĐŊŅƒ ŅĐĩŅ€ĐĩдОвиŅ‰Đ°â€Ļ ВиĐēĐžŅ€Đ¸ŅŅ‚ОвŅƒĐšŅ‚Đĩ ĐŊавŅƒŅˆĐŊиĐēи, ŅĐē СаСвиŅ‡Đ°Đš. Đ¯ĐēŅ‰Đž ŅĐĩŅ€ĐĩдОвиŅ‰Đĩ айО Đ°Ņ‚ĐŧĐžŅŅ„ĐĩŅ€ĐŊиК Ņ‚иŅĐē СĐŧŅ–ĐŊŅŽŅŽŅ‚ŅŒŅŅ, СаĐŋŅƒŅŅ‚Ņ–Ņ‚ŅŒ ĐžĐŋŅ‚иĐŧŅ–СаŅ‚ĐžŅ€ Ņ‰Đĩ Ņ€Đ°Đˇ. ВиĐŧŅ–Ņ€ŅŽĐ˛Đ°ĐŊĐŊŅ Đ°Ņ‚ĐŧĐžŅŅ„ĐĩŅ€ĐŊĐžĐŗĐž Ņ‚иŅĐēŅƒâ€Ļ @@ -1640,24 +1640,24 @@ Galaxy Buds Pro ВŅ–Đ´Ņ‚вОŅ€ĐĩĐŊĐŊŅ виĐēĐģиĐēŅ–в Ņ‡ĐĩŅ€ĐĩС ĐŊавŅƒŅˆĐŊиĐēи, ĐēĐžĐģи вОĐŊи Ņƒ ваŅˆĐ¸Ņ… вŅƒŅ…Đ°Ņ… АвŅ‚ĐžĐŧĐ°Ņ‚иŅ‡ĐŊĐĩ ĐŋĐĩŅ€ĐĩĐŧиĐēĐ°Ņ” ĐŊавŅƒŅˆĐŊиĐēи ĐŧŅ–Đļ ĐŋОв\'ŅĐˇĐ°ĐŊиĐŧи ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅĐŧи - ГŅƒŅ‡ĐŊŅ–ŅŅ‚ŅŒ ĐŊавĐēĐžĐģиŅˆĐŊŅŒĐžĐŗĐž ŅĐĩŅ€ĐĩдОвиŅ‰Đ° СĐģŅ–ва + ГŅƒŅ‡ĐŊŅ–ŅŅ‚ŅŒ ŅĐĩŅ€ĐĩдОвиŅ‰Đ° СĐģŅ–ва НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊавĐēĐžĐģиŅˆĐŊŅŒĐžĐŗĐž СвŅƒĐēŅƒ НавĐēĐžĐģиŅˆĐŊŅ–Đš СвŅƒĐē ĐŋŅ–Đ´ Ņ‡Đ°Ņ виĐēĐģиĐēŅƒ - АĐēŅ‚ивĐŊиК Ņ€Ņ–вĐĩĐŊŅŒ ĐŋŅ€Đ¸ĐŗĐŊŅ–Ņ‡ĐĩĐŊĐŊŅ ŅˆŅƒĐŧŅƒ + Đ Ņ–вĐĩĐŊŅŒ Đ°ĐēŅ‚ивĐŊĐžĐŗĐž ŅˆŅƒĐŧĐžĐŋĐžĐŗĐģиĐŊĐ°ĐŊĐŊŅ ВиŅĐžĐēиК ПĐĩŅ€ĐĩĐŧиĐēĐ°ĐŊĐŊŅ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ° ĐēĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐģŅ–вОŅ€ŅƒŅ‡ ПĐĩŅ€ĐĩĐŧиĐēĐ°ĐŊĐŊŅ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ° ĐēĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŋŅ€Đ°Đ˛ĐžŅ€ŅƒŅ‡ - АĐēŅ‚ивĐŊĐĩ ĐŋŅ€Đ¸ĐŗĐŊŅ–Ņ‡ĐĩĐŊĐŊŅ ŅˆŅƒĐŧŅƒ + АĐēŅ‚ивĐŊĐĩ ŅˆŅƒĐŧĐžĐŋĐžĐŗĐģиĐŊĐ°ĐŊĐŊŅ ГŅƒŅ‡ĐŊŅ–ŅŅ‚ŅŒ ВŅ–Đ´ Đŧ\'ŅĐēĐžĐŗĐž Đ´Đž Ņ‡Đ¸ŅŅ‚ĐžĐŗĐž - ПŅ€Đ¸ĐŗĐŊŅ–Ņ‡ĐĩĐŊĐŊŅ ŅˆŅƒĐŧŅƒ ←→ ВиĐŧĐēĐŊĐĩĐŊĐž - НавĐēĐžĐģиŅˆĐŊŅ” ŅĐĩŅ€ĐĩдОвиŅ‰Đĩ ←→ ВиĐŧĐēĐŊĐĩĐŊĐž + ШŅƒĐŧĐžĐŋĐžĐŗĐģиĐŊĐ°ĐŊĐŊŅ ←→ ВиĐŧĐēĐŊĐĩĐŊĐž + ĐĄĐĩŅ€ĐĩдОвиŅ‰Đĩ ←→ ВиĐŧĐēĐŊĐĩĐŊĐž КоĐŊŅ‚Ņ€ĐžĐģŅŒ ŅˆŅƒĐŧŅƒ ВиŅĐ˛ĐģĐĩĐŊĐŊŅ ĐŗĐžĐģĐžŅŅƒ ПодвŅ–ĐšĐŊĐĩ Ņ‚ĐžŅ€ĐēĐ°ĐŊĐŊŅ ĐēŅ€Đ°ŅŽ ЗавĐĩŅ€ŅˆĐĩĐŊĐŊŅ ĐŋŅ–ŅĐģŅ Ņ‚иŅˆŅ– Са: 15 ŅĐĩĐēŅƒĐŊĐ´ - ГŅƒŅ‡ĐŊŅ–ŅŅ‚ŅŒ ĐŊавĐēĐžĐģиŅˆĐŊŅŒĐžĐŗĐž ŅĐĩŅ€ĐĩдОвиŅ‰Đ° вĐŋŅ€Đ°Đ˛Đž + ГŅƒŅ‡ĐŊŅ–ŅŅ‚ŅŒ ŅĐĩŅ€ĐĩдОвиŅ‰Đ° ŅĐŋŅ€Đ°Đ˛Đ° ЧŅ–Ņ‚ĐēĐž Ņ‡ŅƒŅ‚и вĐģĐ°ŅĐŊиК ĐŗĐžĐģĐžŅ ĐŋŅ–Đ´ Ņ‡Đ°Ņ виĐēĐģиĐēŅ–в ПĐģавĐŊĐĩ ĐŋĐĩŅ€ĐĩĐŧиĐēĐ°ĐŊĐŊŅ С\'Ņ”Đ´ĐŊĐ°ĐŊĐŊŅ ПаŅ€Đ°ĐŧĐĩŅ‚Ņ€Đ¸ ĐŊавĐēĐžĐģиŅˆĐŊŅŒĐžĐŗĐž СвŅƒĐēŅƒ @@ -1671,7 +1671,7 @@ ДозвоĐģиŅ‚и ĐēĐžĐŊŅ‚Ņ€ĐžĐģŅŒ ŅˆŅƒĐŧŅƒ ĐŋŅ–Đ´ Ņ‡Đ°Ņ виĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐŊŅ ĐģиŅˆĐĩ ОдĐŊĐžĐŗĐž ĐŊавŅƒŅˆĐŊиĐēĐ° БаĐģĐ°ĐŊŅ ĐĸĐžĐŊ ĐŊавĐēĐžĐģиŅˆĐŊŅŒĐžĐŗĐž СвŅƒĐēŅƒ - ПŅ€Đ¸ĐŗĐŊŅ–Ņ‡ĐĩĐŊĐŊŅ ŅˆŅƒĐŧŅƒ ←→ НавĐēĐžĐģиŅˆĐŊŅ” ŅĐĩŅ€ĐĩдОвиŅ‰Đĩ + ШŅƒĐŧĐžĐŋĐžĐŗĐģиĐŊĐ°ĐŊĐŊŅ ←→ ĐĄĐĩŅ€ĐĩдОвиŅ‰Đĩ ĐŖвŅ–ĐŧĐēĐŊŅƒŅ‚и ĐŊавĐēĐžĐģиŅˆĐŊŅ–Đš СвŅƒĐē Ņ– авŅ‚ĐžĐŧĐ°Ņ‚иŅ‡ĐŊĐĩ ŅŅ‚иŅˆĐĩĐŊĐŊŅ вŅ–Đ´Ņ‚вОŅ€ĐĩĐŊĐŊŅ ĐŋŅ–ŅĐģŅ виŅĐ˛ĐģĐĩĐŊĐŊŅ ĐŗĐžĐģĐžŅŅƒ ВиŅĐ˛ĐģĐĩĐŊĐŊŅ ĐŋОдвŅ–ĐšĐŊĐžĐŗĐž Ņ‚ĐžŅ€ĐēĐ°ĐŊĐŊŅ, ĐŊавŅ–Ņ‚ŅŒ ŅĐēŅ‰Đž ĐŊĐĩ Ņ‚ĐžŅ€ĐēĐ°Ņ‚иŅŅ Ņ‚Đ°Ņ‡ĐŋĐ°Đ´Đ° 10 ŅĐĩĐēŅƒĐŊĐ´ @@ -2094,13 +2094,13 @@ ДозвоĐģиŅ‚и Ņ–ĐŊŅ–Ņ†Ņ–Đ°Ņ†Ņ–ŅŽ ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊŅ–СаŅ†Ņ–Ņ— Đ´Ņ–Đš ДозвоĐģиŅ‚и Ņ–ĐŊŅ–Ņ†Ņ–ŅŽĐ˛Đ°ĐŊĐŊŅ ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊŅ–СаŅ†Ņ–Ņ— Đ°ĐēŅ‚ивĐŊĐžŅŅ‚Ņ– Ņ‡ĐĩŅ€ĐĩС Intent API ДозвоĐģиŅ‚и Ņ–ĐŊŅ–Ņ†Ņ–ŅŽĐ˛Đ°Ņ‚и ĐĩĐēŅĐŋĐžŅ€Ņ‚ йаСи Đ´Đ°ĐŊиŅ… Ņ‡ĐĩŅ€ĐĩС Intent API - ШŅƒĐŧОСаĐŗĐģŅƒŅˆĐĩĐŊĐŊŅ, НавĐēĐžĐģиŅˆĐŊŅ–Đš СвŅƒĐē, ВиĐŧĐē - ШŅƒĐŧОСаĐŗĐģŅƒŅˆĐĩĐŊĐŊŅ, ĐŊавĐēĐžĐģиŅˆĐŊŅ–Đš СвŅƒĐē + ШŅƒĐŧĐžĐŋĐžĐŗĐģиĐŊĐ°ĐŊĐŊŅ, НавĐēĐžĐģиŅˆĐŊŅ–Đš СвŅƒĐē, ВиĐŧĐē + ШŅƒĐŧĐžĐŋĐžĐŗĐģиĐŊĐ°ĐŊĐŊŅ, ĐŊавĐēĐžĐģиŅˆĐŊŅ–Đš СвŅƒĐē НавĐēĐžĐģиŅˆĐŊŅ–Đš СвŅƒĐē, ВиĐŧĐē ШвидĐēиК Đ´ĐžŅŅ‚ŅƒĐŋ (ĐŋОдвŅ–ĐšĐŊĐĩ Ņ‚ĐžŅ€ĐēĐ°ĐŊĐŊŅ) ШвидĐēиК Đ´ĐžŅŅ‚ŅƒĐŋ (ĐŋĐžŅ‚Ņ€Ņ–ĐšĐŊĐĩ Ņ‚ĐžŅ€ĐēĐ°ĐŊĐŊŅ) Đ ĐĩĐļиĐŧи ĐēĐŊĐžĐŋĐēи ĐēĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊавĐēĐžĐģиŅˆĐŊŅ–Đŧ СвŅƒĐēĐžĐŧ - ШŅƒĐŧОСаĐŗĐģŅƒŅˆĐĩĐŊĐŊŅ, ВиĐŧĐē + ШŅƒĐŧĐžĐŋĐžĐŗĐģиĐŊĐ°ĐŊĐŊŅ, ВиĐŧĐē WeChat Pay ЧаŅ ĐžĐŊОвĐģĐĩĐŊĐŊŅ AGPS ĐĸĐĩŅ€ĐŧŅ–ĐŊ Đ´Ņ–Ņ— AGPS @@ -2151,7 +2151,7 @@ ЗŅƒĐŋиĐŊиŅ‚и FTP-ŅĐĩŅ€Đ˛ĐĩŅ€ С ĐŗОдиĐŊĐŊиĐēĐ° ĐĄŅ‚Đ°ĐŊ FTP-ŅĐĩŅ€Đ˛ĐĩŅ€Đ° ĐĨĐžŅ€Đ˛Đ°Ņ‚ŅŅŒĐēĐ° - АвŅ‚ĐžĐŧĐ°Ņ‚иŅ‡ĐŊĐž виĐŧиĐēĐ°Ņ‚и ŅˆŅƒĐŧОСаĐŗĐģŅƒŅˆĐĩĐŊĐŊŅ, ĐēĐžĐģи ви ĐŋĐžŅ‡Đ¸ĐŊĐ°Ņ”Ņ‚Đĩ ĐŗОвОŅ€Đ¸Ņ‚и. + АвŅ‚ĐžĐŧĐ°Ņ‚иŅ‡ĐŊĐž виĐŧиĐēĐ°Ņ‚и ŅˆŅƒĐŧĐžĐŋĐžĐŗĐģиĐŊĐ°ĐŊĐŊŅ, ĐēĐžĐģи ви ĐŋĐžŅ‡Đ¸ĐŊĐ°Ņ”Ņ‚Đĩ ĐŗОвОŅ€Đ¸Ņ‚и. Đ Đ°ĐŊĐēОвŅ– ĐžĐŊОвĐģĐĩĐŊĐŊŅ ПоĐēаСŅƒĐ˛Đ°Ņ‚и ĐžĐŊОвĐģĐĩĐŊĐŊŅ Ņ‰ĐžŅ€Đ°ĐŊĐēŅƒ КаŅ‚ĐĩĐŗĐžŅ€Ņ–Ņ— Ņ€Đ°ĐŊĐēОвиŅ… ĐžĐŊОвĐģĐĩĐŊŅŒ @@ -2180,7 +2180,7 @@ ПŅ€ĐžĐ´ĐžĐ˛ĐļиŅ‚и ĐŊĐ°Ņ‚иŅĐēĐ°ĐŊĐŊŅ ШвидĐēĐ° ŅƒĐ˛Đ°ĐŗĐ° ЗаŅ‚иŅĐēĐ°ĐŊĐŊŅ - ШŅƒĐŧОСаĐŗĐģŅƒŅˆĐĩĐŊĐŊŅ ←→ НавĐēĐžĐģиŅˆĐŊŅ” ŅĐĩŅ€ĐĩдОвиŅ‰Đĩ ←→ ВиĐŧĐēĐŊĐĩĐŊĐž + ШŅƒĐŧĐžĐŋĐžĐŗĐģиĐŊĐ°ĐŊĐŊŅ ←→ ĐĄĐĩŅ€ĐĩдОвиŅ‰Đĩ ←→ ВиĐŧĐēĐŊĐĩĐŊĐž Đ ĐĩĐļиĐŧи ĐēĐŊĐžĐŋĐžĐē - ДовŅ–Đ´ĐēĐ° MI AI КаŅ€Ņ‚Đēи @@ -2235,7 +2235,7 @@ ĐŖ ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ— ĐŊĐĩĐŧĐ°Ņ” вŅ–ĐģŅŒĐŊиŅ… ŅĐģĐžŅ‚Ņ–в Đ´ĐģŅ ĐēĐžĐŊŅ‚Đ°ĐēŅ‚Ņ–в (ŅƒŅŅŒĐžĐŗĐž ŅĐģĐžŅ‚Ņ–в: %1$s) ІĐŧ\'Ņ КоĐŊŅ‚Đ°ĐēŅ‚ĐŊŅ– Đ´Đ°ĐŊŅ– - Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ видаĐģиŅ‚и \'%1$s\'\? + Ви Đ´Ņ–ĐšŅĐŊĐž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ видаĐģиŅ‚и \'%1$s\'? КоĐŊŅ‚Đ°ĐēŅ‚ĐŊиК ĐŊĐžĐŧĐĩŅ€ ĐŋĐžŅ€ĐžĐļĐŊŅ–Đš НавŅ–ĐŗĐ°Ņ†Ņ–Ņ НадŅĐ¸ĐģĐ°Ņ‚и ĐŊавŅ–ĐŗĐ°Ņ†Ņ–ŅŽ ĐŊĐ° ĐŗОдиĐŊĐŊиĐē @@ -2279,8 +2279,8 @@ PAI ĐŊĐ° ĐŧŅ–ŅŅŅ†ŅŒ +%d %d Ņ…в - ДĐĩĐŊĐŊиК ĐŋŅ€Đ¸Ņ€Ņ–ŅŅ‚ PAI - ВŅŅŒĐžĐŗĐž PAI + ДĐĩĐŊĐŊиК ĐŋŅ€Đ¸Ņ€Ņ–ŅŅ‚ + ĐŖŅŅŒĐžĐŗĐž Назва ĐŋĐ°ĐēŅƒĐŊĐēĐ° Catima ĐŖŅŅ‚Đ°ĐŊОвиŅ‚и Catima ХиĐŊŅ…Ņ€ĐžĐŊŅ–СŅƒĐ˛Đ°Ņ‚и ĐģиŅˆĐĩ ĐŋĐĩвĐŊŅ– ĐŗŅ€ŅƒĐŋи @@ -2503,4 +2503,158 @@ ЧаŅŅ‚Ņ– ŅĐ¸ĐŧвОĐģи ІĐŗĐŊĐžŅ€ŅƒĐ˛Đ°Ņ‚и ŅĐŋОвŅ–Ņ‰ĐĩĐŊĐŊŅ Ņ€ĐžĐąĐžŅ‡ĐžĐŗĐž ĐŋŅ€ĐžŅ„Ņ–ĐģŅŽ НĐĩ ĐŊĐ°Đ´ŅĐ¸ĐģĐ°Ņ‚и ŅĐŋОвŅ–Ņ‰ĐĩĐŊĐŊŅ Ņ–С СаŅŅ‚ĐžŅŅƒĐŊĐēŅ–в С Ņ€ĐžĐąĐžŅ‡ĐžĐŗĐž ĐŋŅ€ĐžŅ„Ņ–ĐģŅŽ ĐŊĐ° ĐŗОдиĐŊĐŊиĐē + ЧиŅŅ‚иК ĐąŅ–ĐģиК + ІĐŗĐŊĐžŅ€ŅƒĐ˛Đ°Ņ‚и (Ņ‚иŅˆĐ°) + ĐĄĐĩŅ€ĐĩĐ´ĐŊŅ Ņ‡Đ°ŅŅ‚ĐžŅ‚Đ° ĐŧĐ°Ņ…Ņ–в + Đ Ņ–ŅˆŅƒŅ‡Ņ–ŅŅ‚ŅŒ + МŅ–ŅŅ‚Đž ŅˆĐ˛Đ¸Đ´ĐēĐžŅŅ‚Ņ– + НавŅƒŅˆĐŊиĐēи + ЗвиŅ‡Đ°ĐšĐŊиК (60 -90Ņ) + ВŅ–Đ´Ņ…иĐģиŅ‚и + ВŅ–ĐģŅŒĐŊĐĩ ĐŋĐžŅ”Đ´ĐŊĐ°ĐŊĐŊŅ + ĐĄĐēĐģĐ°Đ´ Ņ‚Ņ–ĐģĐ° + Zepp Pay + ĐĄĐŋĐžŅŅ–Đą вŅ–Đ´Ņ…иĐģĐĩĐŊĐŊŅ виĐēĐģиĐēŅƒ + ОŅ‚Ņ€Đ¸ĐŧĐ°ĐŊĐŊŅ ŅŅ‚Đ°Ņ‚иŅŅ‚иĐēи + ШвидĐēиК (30Ņ) + ЗоŅ€ŅĐŊĐĩ ĐŊĐĩйО + Amazfit Balance + НавŅ–ĐŗĐ°Ņ†Ņ–ŅŽ СаĐŋŅƒŅ‰ĐĩĐŊĐž, Đ°ĐģĐĩ СаŅŅ‚ĐžŅŅƒĐŊĐžĐē ĐŊавŅ–ĐŗĐ°Ņ†Ņ–Ņ— ĐŊĐĩ вŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐž ĐŊĐ° ĐŗОдиĐŊĐŊиĐēŅƒ. ĐŖŅŅ‚Đ°ĐŊОвŅ–Ņ‚ŅŒ КОĐŗĐž С МĐĩĐŊĐĩĐ´ĐļĐĩŅ€Đ° СаŅŅ‚ĐžŅŅƒĐŊĐēŅ–в. + ŅŅ€Đ´Đ¸ + ĐŧĐ°Ņ…/Ņ…в + Đ ĐĩĐļиĐŧ виĐŧŅ–Ņ€ŅŽĐ˛Đ°ĐŊĐŊŅ + Đ¯ĐēĐ° Đ´Ņ–Ņ виĐēĐžĐŊŅƒŅ”Ņ‚ŅŒŅŅ, ĐēĐžĐģи вŅ…Ņ–Đ´ĐŊиК виĐēĐģиĐē вŅ–Đ´Ņ…иĐģŅŅ”Ņ‚ŅŒŅŅ С ĐŗОдиĐŊĐŊиĐēĐ° + Amazfit Active Edge + ОŅ‚Ņ€Đ¸ĐŧĐ°ĐŊĐŊŅ Đ´Đ°ĐŊиŅ… ĐŋŅ€Đž Ņ‚ĐĩĐŧĐŋĐĩŅ€Đ°Ņ‚ŅƒŅ€Ņƒ + ĐĸĐžŅ‡ĐŊиК (3 Ņ…в) + БĐĩСĐēŅ€Đ°Ņ” ĐŊĐĩйО + Đ¯Ņ€ĐģиĐēи Ņ‚Ņ€ĐĩĐŊŅƒĐ˛Đ°ĐŊŅŒ + МаĐēŅĐ¸ĐŧĐ°ĐģŅŒĐŊĐ° Ņ‡Đ°ŅŅ‚ĐžŅ‚Đ° ĐŧĐ°Ņ…Ņ–в + ĐĄĐŋĐ°ĐģĐ°Ņ… ĐąĐģиŅĐēавĐēи + ДовĐļиĐŊĐ° Đ´ĐžŅ€Ņ–ĐļĐēи + ĐĄĐĩŅ€ĐĩĐ´ĐŊŅ–Đš Ņ€Ņ–вĐĩĐŊŅŒ ĐēиŅĐŊŅŽ в ĐēŅ€ĐžĐ˛Ņ– + ĐŖŅŅŒĐžĐŗĐž ĐŧĐ°Ņ…Ņ–в + Đ¯Ņ€ĐģиĐēи СаŅŅ‚ĐžŅŅƒĐŊĐēŅ–в + ПоводиŅ€ + ГоŅ‚ОвĐŊŅ–ŅŅ‚ŅŒ + Amazfit Active + ĐĸĐĩŅ€ĐŧĐžĐŧĐĩŅ‚Ņ€ + Femometer Vinca II + На ĐŗОдиĐŊĐŊиĐēŅƒ ĐŊĐĩ вŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐž ĐŊавŅ–ĐŗĐ°Ņ†Ņ–ĐšĐŊиК СаŅŅ‚ĐžŅŅƒĐŊĐžĐē + ІĐŊŅˆŅ– ŅĐŋОвŅ–Ņ‰ĐĩĐŊĐŊŅ + ĐĄĐŋОвŅ–Ņ‰ĐĩĐŊĐŊŅ ĐēĐ°ĐģĐĩĐŊĐ´Đ°Ņ€Ņ + ĐĄĐŋОвŅ–Ņ‰ĐĩĐŊĐŊŅ ĐŋŅ€Đž вŅ…Ņ–Đ´ĐŊŅ– виĐēĐģиĐēи (вŅ–ĐąŅ€Đ°Ņ†Ņ–Ņ/ĐŋиŅ‰Đ°ĐŊĐŊŅ) + НĐĩ Ņ‚ŅƒŅ€ĐąŅƒĐ˛Đ°Ņ‚и - ĐŖвŅ–ĐŧĐēĐŊĐĩĐŊĐž + БĐĩССвŅƒŅ‡ĐŊиК Ņ€ĐĩĐļиĐŧ Ņ‚ĐĩĐģĐĩŅ„ĐžĐŊĐ° + ĐĸиĐŋ ĐŊĐžŅŅ–ĐŊĐŊŅ + Pebble (ĐŋŅ€ŅĐļĐēĐ° ĐŊĐ° вСŅƒŅ‚Ņ‚Ņ–) + Xiaomi Smart Band 8 + ĐĄĐŋОвŅ–Ņ‰ĐĩĐŊĐŊŅ ĐŋŅ€Đž вŅ…Ņ–Đ´ĐŊŅ– виĐēĐģиĐēи + ЗвиŅ‡Đ°ĐšĐŊиК / ВŅ–ĐąŅ€Đ°Ņ†Ņ–Ņ + ДодаŅ‚ĐēОва ĐŧĐĩŅ‚Đ° + ĐĄĐŋОвŅ–Ņ‰ĐĩĐŊĐŊŅ Đĩ-ĐŋĐžŅˆŅ‚и + ПоŅ…Ņ–Đ´ Ņƒ ĐŗĐžŅ€Đ¸ + ĐĄĐŋОвŅ–Ņ‰ĐĩĐŊĐŊŅ Đĩ-ĐŋĐžŅˆŅ‚и (вŅ–ĐąŅ€Đ°Ņ†Ņ–Ņ/ĐŋиŅ‰Đ°ĐŊĐŊŅ) + НĐĩ Ņ‚ŅƒŅ€ĐąŅƒĐ˛Đ°Ņ‚и - ЛиŅˆĐĩ ĐąŅƒĐ´Đ¸ĐģŅŒĐŊиĐē + ĐĄĐŋОвŅ–Ņ‰ĐĩĐŊĐŊŅ ĐŋŅ€Đž СМС (вŅ–ĐąŅ€Đ°Ņ†Ņ–Ņ/ĐŋиŅ‰Đ°ĐŊĐŊŅ) + ColaCao 2023 + ПоĐēаСаŅ‚и ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅ–Đš ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´ ĐŋОвŅ–Đ´ĐžĐŧĐģĐĩĐŊĐŊŅ в СаĐŗĐžĐģОвĐēŅƒ + Redmi Watch 3 Active + ВŅ–ĐąŅ€Đ°Ņ†Ņ–Ņ / БĐĩССвŅƒŅ‡ĐŊиК + Redmi Smart Band 2 + ĐĄĐŋОвŅ–Ņ‰ĐĩĐŊĐŊŅ ĐēĐ°ĐģĐĩĐŊĐ´Đ°Ņ€Ņ (вŅ–ĐąŅ€Đ°Ņ†Ņ–Ņ/ĐŋиŅ‰Đ°ĐŊĐŊŅ) + НадŅ–ŅĐģĐ°Ņ‚и ŅĐŋОвŅ–Ņ‰ĐĩĐŊĐŊŅ СаŅŅ‚ĐžŅŅƒĐŊĐēŅƒ ĐŊĐ° ĐŋŅ€Đ¸ŅŅ‚Ņ€Ņ–Đš + НĐĩ Ņ‚ŅƒŅ€ĐąŅƒĐ˛Đ°Ņ‚и - ЛиŅˆĐĩ ваĐļĐģивŅ– + ДаĐŊŅŅŒĐēĐ° + Xiaomi Smart Band 7 Pro + ĐĄĐŋОвŅ–Ņ‰ĐĩĐŊĐŊŅ Ņ–ĐŊŅˆĐ¸Ņ… ĐēĐ°Ņ‚ĐĩĐŗĐžŅ€Ņ–Đš (вŅ–ĐąŅ€Đ°Ņ†Ņ–Ņ/ĐŋиŅ‰Đ°ĐŊĐŊŅ) + ЧаŅ ŅŅ‚ĐžŅŅ‡Đ¸ + ЧаŅ Đ´Ņ–ŅĐģŅŒĐŊĐžŅŅ‚Ņ– + Назва СаŅŅ‚ĐžŅŅƒĐŊĐēŅƒ в ŅĐŋОвŅ–Ņ‰ĐĩĐŊĐŊŅ– + НаŅ€ŅƒŅ‡ĐŊиК ĐąŅ€Đ°ŅĐģĐĩŅ‚ + ColaCao 2021 + КоĐģŅŒŅ” (ŅˆĐ¸ĐšĐŊиК Ņ€ĐĩĐŧŅ–ĐŊĐĩŅ†ŅŒ) + Xiaomi Watch Lite + ПоĐēаСаŅ‚и ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅ–Đš ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´ ĐŋОвŅ–Đ´ĐžĐŧĐģĐĩĐŊĐŊŅ в СаĐŗĐžĐģОвĐēŅƒ ĐŋОвŅ–Đ´ĐžĐŧĐģĐĩĐŊĐŊŅ, ŅĐē Ņ†Đĩ дОСвОĐģĐĩĐŊĐž ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ”Đŧ + НадŅ–ŅĐģĐ°Ņ‚и ŅĐŋОвŅ–Ņ‰ĐĩĐŊĐŊŅ + OsmAnd(+) + ĐĄĐŋОвŅ–Ņ‰ĐĩĐŊĐŊŅ ĐŋŅ€Đž СМС + НĐĩ Ņ‚ŅƒŅ€ĐąŅƒĐ˛Đ°Ņ‚и - ВиĐŧĐēĐŊĐĩĐŊĐž + ЗвиŅ‡Đ°ĐšĐŊиК / БĐĩССвŅƒŅ‡ĐŊиК + ĐĸŅ€ĐĩĐšĐģŅ€Đ°ĐŊŅ–ĐŊĐŗ + Mi Watch Color Sport + НавŅ–ĐŗĐ°Ņ†Ņ–ĐšĐŊŅ– СаŅŅ‚ĐžŅŅƒĐŊĐēи + БŅ–Đŗ + ГŅ€Đ°Ņ„Ņ–Đē Ņ€ĐĩĐļиĐŧŅƒ ŅĐŊŅƒ + ПоĐŋĐģĐĩŅĐēĐ°ĐšŅ‚Đĩ в Đ´ĐžĐģĐžĐŊŅ–, Ņ‰ĐžĐą ŅƒĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚и ĐĩĐēŅ€Đ°ĐŊ" + ПоŅ‡Đ°Ņ‚ĐžĐē ŅĐŊŅƒ + ОŅ‚Ņ€Đ¸ĐŧŅƒĐšŅ‚Đĩ ŅĐŋОвŅ–Ņ‰ĐĩĐŊĐŊŅ, ĐēĐžĐģи ваŅˆ ĐŋĐžĐēаСĐŊиĐē ĐļиŅ‚Ņ‚Ņ”вОŅ— ĐĩĐŊĐĩŅ€ĐŗŅ–Ņ— Đ´ĐžŅŅĐŗĐŊĐĩ 30, 60 айО 100 Са ĐžŅŅ‚Đ°ĐŊĐŊŅ– 7 Đ´ĐŊŅ–в + Pixoo + ОŅ†Ņ–ĐŊĐēĐ° ĐļиŅ‚Ņ‚Ņ”вОŅ— ĐĩĐŊĐĩŅ€ĐŗŅ–Ņ— + НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊавŅ–ĐŗĐ°Ņ†Ņ–Ņ— + ЗоŅĐĩŅ€ĐĩĐ´ĐļĐĩĐŊĐŊŅ + КаŅ€Ņ‚и Google + ПовŅ‚ĐžŅ€ĐŊĐĩ ĐŋĐģĐĩŅĐēĐ°ĐŊĐŊŅ виĐŧĐēĐŊĐĩ ĐĩĐēŅ€Đ°ĐŊ" + Mijia Temperature Ņ– Humidity Sensor 2 + ПŅ€ĐĩŅ„Ņ–ĐēŅ ĐŊаСви ŅĐŋОвŅ–Ņ‰ĐĩĐŊĐŊŅ С ĐŊаСвОŅŽ СаŅŅ‚ĐžŅŅƒĐŊĐēŅƒ-Đ´ĐļĐĩŅ€ĐĩĐģĐ° + ĐĄĐĩŅ€Ņ–ĐšĐŊиК ĐŊĐžĐŧĐĩŅ€ + ВиĐēĐžŅ€Đ¸ŅŅ‚ОвŅƒŅ”Ņ‚ŅŒŅŅ Đ´ĐģŅ вийОŅ€Ņƒ вĐĩŅ€ŅŅ–Ņ— OsmAnd Đ´ĐģŅ ĐŋŅ–Đ´\'Ņ”Đ´ĐŊĐ°ĐŊĐŊŅ + ПŅ€ĐžĐąŅƒĐ´ĐļĐĩĐŊĐŊŅ + ĐĄŅ‚Đ°Ņ‚иŅŅ‚иĐēĐ° + НĐĩ ĐŊĐ°ĐģĐ°ŅˆŅ‚ОваĐŊĐž + Назва ĐŋĐ°ĐēŅƒĐŊĐēĐ° OsmAnd + ЕĐēŅ€Đ°ĐŊ виĐŧĐēĐŊĐĩŅ‚ŅŒŅŅ ĐŋŅ–ŅĐģŅ Ņ‚ĐžĐŗĐž, ŅĐē ĐŧŅ–ĐēŅ€ĐžŅ„ĐžĐŊ СаŅ„Ņ–ĐēŅŅƒŅ” Ņ‚иŅˆŅƒ ĐŋŅ€ĐžŅ‚ŅĐŗĐžĐŧ ĐŋĐĩвĐŊĐžĐŗĐž Ņ‡Đ°ŅŅƒ + НадŅĐ¸ĐģĐ°Ņ‚и ĐŊĐ°ĐŗĐ°Đ´ŅƒĐ˛Đ°ĐŊĐŊŅ Ņ‚Đ° ĐŋĐĩŅ€ĐĩŅ…ОдиŅ‚и в Ņ€ĐĩĐļиĐŧ ŅĐŊŅƒ ĐŋĐĩŅ€ĐĩĐ´ ŅĐŊĐžĐŧ. ĐŖ СаĐŋĐģĐ°ĐŊОваĐŊиК Ņ‡Đ°Ņ ĐŋŅ€ĐžĐąŅƒĐ´ĐļĐĩĐŊĐŊŅ ĐŋŅ€ĐžĐģŅƒĐŊĐ°Ņ” ĐąŅƒĐ´Đ¸ĐģŅŒĐŊиĐē. + ПŅ€ĐžĐŗŅ€ĐĩŅ Са 7 Đ´ĐŊŅ–в + ОĐŋОвŅ–Ņ‰ĐĩĐŊĐŊŅ + БоŅ€ĐžŅ‚ŅŒĐąĐ° + Xiaomi Watch S1 Active + ĐŖ ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ— ĐŊĐĩĐŧĐ°Ņ” вŅ–ĐģŅŒĐŊиŅ… ŅĐģĐžŅ‚Ņ–в Đ´ĐģŅ ĐĩĐēŅ€Đ°ĐŊŅ–в вŅ–Đ´ĐļĐĩŅ‚Ņ–в (ŅƒŅŅŒĐžĐŗĐž ŅĐģĐžŅ‚Ņ–в: %1$s) + Ви Đ´Ņ–ĐšŅĐŊĐž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ видаĐģиŅ‚и \'%1$s\'? + ВидаĐģиŅ‚и ĐĩĐēŅ€Đ°ĐŊ вŅ–Đ´ĐļĐĩŅ‚Đ° + ВŅ–Đ´ĐļĐĩŅ‚ + ОŅ‚Ņ€Đ¸ĐŧŅƒĐ˛Đ°Ņ‚и ŅĐŋОвŅ–Ņ‰ĐĩĐŊĐŊŅ, ĐēĐžĐģи ви Đ´ĐžŅŅĐŗĐģи ĐŧĐ°ĐēŅĐ¸ĐŧĐ°ĐģŅŒĐŊĐžŅ— ĐēŅ–ĐģŅŒĐēĐžŅŅ‚Ņ– йаĐģŅ–в ĐļиŅ‚Ņ‚Ņ”вОŅ— ĐĩĐŊĐĩŅ€ĐŗŅ–Ņ— Са Đ´ĐĩĐŊŅŒ + ЕĐēŅ€Đ°ĐŊ вŅ–Đ´ĐļĐĩŅ‚Đ° + ЊОдĐĩĐŊĐŊиК ĐŋŅ€ĐžĐŗŅ€ĐĩŅ + МаĐēĐĩŅ‚ вŅ–Đ´ĐļĐĩŅ‚Đ° + НĐĩвŅ–Đ´ĐžĐŧиК Ņ‚иĐŋ Ņ‚Ņ€ĐĩĐŊŅƒĐ˛Đ°ĐŊĐŊŅ - %s + ПĐĩŅ€ĐĩĐŧŅ–ŅŅ‚иŅ‚и виŅ‰Đĩ + ІĐŊŅŅ‚Ņ€ŅƒĐŧĐĩĐŊŅ‚и ĐŊавŅ–ĐŗĐ°Ņ†Ņ–Ņ— + НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°Ņ‚и ĐŋОвĐĩĐ´Ņ–ĐŊĐēŅƒ ĐŊавŅ–ĐŗĐ°Ņ†Ņ–ĐšĐŊĐžĐŗĐž СаŅŅ‚ĐžŅŅƒĐŊĐēŅƒ ĐŊĐ° ĐŗОдиĐŊĐŊиĐēŅƒ + Чи ĐŋОвиĐŊĐĩĐŊ ĐŊавŅ–ĐŗĐ°Ņ†Ņ–ĐšĐŊиК СаŅŅ‚ĐžŅŅƒĐŊĐžĐē авŅ‚ĐžĐŧĐ°Ņ‚иŅ‡ĐŊĐž вŅ–Đ´ĐēŅ€Đ¸Đ˛Đ°Ņ‚иŅŅ ĐŋОвĐĩŅ€Ņ… Ņ–ĐŊŅˆĐ¸Ņ… вŅ–ĐēĐžĐŊ, ĐēĐžĐģи вŅ–ĐŊ ĐžŅ‚Ņ€Đ¸ĐŧŅƒŅ” ĐŊавŅ–ĐŗĐ°Ņ†Ņ–ĐšĐŊŅ– вĐēаСŅ–вĐēи + 2 вŅ–Đ´ĐļĐĩŅ‚и + 1 вĐŗĐžŅ€Ņ–, 2 вĐŊиСŅƒ + 2 вĐŗĐžŅ€Ņ–, 2 вĐŊиСŅƒ + МаŅ” ĐąŅƒŅ‚и ĐŋŅ€Đ¸ĐŊĐ°ĐšĐŧĐŊŅ– %1$s ĐĩĐēŅ€Đ°ĐŊŅ–в + ВŅ–ĐąŅ€ŅƒĐ˛Đ°Ņ‚и ĐžŅ‚Ņ€Đ¸ĐŧавŅˆĐ¸ ĐŊОвŅƒ вĐēаСŅ–вĐēŅƒ + ПĐĩŅ€ĐĩĐŧŅ–ŅŅ‚иŅ‚и вĐŊиС + 1 вŅ–Đ´ĐļĐĩŅ‚ + ПĐĩŅ€ĐĩĐšŅ‚и ĐŋОвĐĩŅ€Ņ… ŅƒŅŅ–Ņ… вŅ–ĐēĐžĐŊ + ПŅ–Đ´Ņ‚иĐŋ вŅ–Đ´ĐļĐĩŅ‚Đ° + ЕĐēŅ€Đ°ĐŊ %s + ВибĐĩŅ€Ņ–Ņ‚ŅŒ ŅƒŅŅ– вŅ–Đ´ĐļĐĩŅ‚и + 2 вĐŗĐžŅ€Ņ–, 1 вĐŊиСŅƒ + Чи ĐŋОвиĐŊĐĩĐŊ ĐŗОдиĐŊĐŊиĐē вŅ–ĐąŅ€ŅƒĐ˛Đ°Ņ‚и Са ĐēĐžĐļĐŊĐžŅŽ ĐŊОвОŅŽ айО СĐŧŅ–ĐŊĐĩĐŊĐžŅŽ ĐŊавŅ–ĐŗĐ°Ņ†Ņ–ĐšĐŊĐžŅŽ вĐēаСŅ–вĐēĐžŅŽ (Ņ‚Ņ–ĐģŅŒĐēи ĐēĐžĐģи СаŅŅ‚ĐžŅŅƒĐŊĐžĐē ĐŋĐžĐēаСаĐŊĐž ĐŋОвĐĩŅ€Ņ… Ņ–ĐŊŅˆĐ¸Ņ… вŅ–ĐēĐžĐŊ) + Назва ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ + Redmi Watch 2 Lite + Redmi Smart Band Pro + Nothing Ear (2) + ЛĐĩĐŗĐēĐĩ Đ°ĐēŅ‚ивĐŊĐĩ ŅˆŅƒĐŧĐžĐŋĐžĐŗĐģиĐŊĐ°ĐŊĐŊŅ + ĐĄŅ‚Đ°ĐŊ С\'Ņ”Đ´ĐŊĐ°ĐŊĐŊŅ + Nothing Ear (Stick) + ПŅ€ĐžĐˇĐžŅ€Ņ–ŅŅ‚ŅŒ + ЗаĐŊиСŅŒĐēиК Ņ€Ņ–вĐĩĐŊŅŒ СаŅ€ŅĐ´Ņƒ Đ°ĐēŅƒĐŧŅƒĐģŅŅ‚ĐžŅ€Đ° ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ + ЛиŅˆĐĩ ŅĐēŅ‰Đž ввŅ–ĐŧĐēĐŊĐĩĐŊĐž Đ°ĐēŅ‚ивŅƒĐ˛Đ°ĐŊĐŊŅ ĐŋŅ–Đ´ĐŊĐĩŅĐĩĐŊĐŊŅĐŧ Ņ€ŅƒĐēи + ПаŅ€ĐžĐģŅŒ ĐŋОвиĐŊĐĩĐŊ ŅĐēĐģĐ°Đ´Đ°Ņ‚иŅŅ С 4 Ņ†Đ¸Ņ„Ņ€, Ņ– ĐŧŅ–ŅŅ‚иŅ‚и ĐģиŅˆĐĩ Ņ†Đ¸Ņ„Ņ€Đ¸ + ПовŅ‚ĐžŅ€ĐŊĐž С\'Ņ”Đ´ĐŊĐ°Ņ‚иŅŅ ĐģиŅˆĐĩ С ĐŋŅ–Đ´\'Ņ”Đ´ĐŊĐ°ĐŊиĐŧи ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅĐŧи, СаĐŧŅ–ŅŅ‚ŅŒ С\'Ņ”Đ´ĐŊĐ°ĐŊĐŊŅ С ŅƒŅŅ–ĐŧĐ° ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅĐŧи + ĐĄĐŋОвŅ–Ņ‰ĐĩĐŊĐŊŅ ĐŊĐ° ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ— ĐŋŅ–ŅĐģŅ вŅ–Đ´\'Ņ”Đ´ĐŊĐ°ĐŊĐŊŅ вŅ–Đ´ BT. + 155 ŅƒĐ´/Ņ…в + 165 ŅƒĐ´/Ņ…в + 175 ŅƒĐ´/Ņ…в + 185 ŅƒĐ´/Ņ…в + 195 ŅƒĐ´/Ņ…в + 205 ŅƒĐ´/Ņ…в + Bluetooth Classic + ПовŅ‚ĐžŅ€ĐŊĐž ĐŋŅ–Đ´\'Ņ”Đ´ĐŊŅƒĐ˛Đ°Ņ‚иŅŅŒ ĐģиŅˆĐĩ Đ´Đž ĐŋОв\'ŅĐˇĐ°ĐŊиŅ… ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—в + Bluetooth LE \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 880bdb1e2..3c4c9400c 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -2271,8 +2271,8 @@ %d 分钟 每周 PAI +%d - æ€ģ莥 PAI - 每æ—Ĩ AAI åĸžé•ŋ + æ€ģ莥 + 每æ—Ĩåĸžé•ŋ 需čĻ Catima æĨįŽĄį†äŧšå‘˜åĄ Catima 包名 厉čŖ… Catima @@ -2530,4 +2530,277 @@ æ­Ŗåœ¨čŽˇå–įģŸčŽĄæ•°æŽ 拒æŽĨæ‰‹čĄ¨æĨį”ĩæ—ļ采取äģ€äšˆæ“äŊœ æ­Ŗåœ¨čŽˇå–æ¸ŠåēĻ数捎 + čˇƒæˆ‘æ´ģåŠ¨čžšį•Œ + čĄ€æ°§åšŗ均å€ŧ + čˇƒæˆ‘æ´ģ动 + å¯ŧčˆĒåē”į”¨ + å¯ŧčˆĒéĻ–选饚 + č°ˇæ­Œåœ°å›ž + 在通įŸĨ标éĸ˜å‰æˇģ加æĨč‡Ēåē”į”¨åį§°įš„前įŧ€ + 选拊į”¨äēŽčĻčŋžæŽĨįš„ OsmAnd į‰ˆæœŦ + 通įŸĨ中įš„åē”į”¨å + OsmAnd 包名 + OsmAnd(+) + å…ļäģ–通įŸĨ提醒 + æ—Ĩ历通įŸĨ提醒 + æĨį”ĩ提醒īŧˆæŒ¯åŠ¨/é¸ŖåĢīŧ‰ + 小įąŗ手įŽ¯8 + 跑æ­Ĩ + įĄįœ æ¨ĄåŧčŽĄåˆ’ + æĨį”ĩ提醒 + įĄįœ  + į”ĩ子邎äģļ通įŸĨ提醒 + į”ĩ子邎äģļ通įŸĨ提醒īŧˆæŒ¯åŠ¨/é¸ŖåĢīŧ‰ + 专æŗ¨ + SMSīŧˆįŸ­äŋĄīŧ‰é€šįŸĨįš„提醒īŧˆæŒ¯åŠ¨/é¸ŖåĢīŧ‰ + 在标éĸ˜ä¸­æ˜žį¤ēæļˆæ¯įš„éĸ„č§ˆ + įēĸįąŗæ‰‹čĄ¨3 čŋåŠ¨į‰ˆ + æ—Ĩ历通įŸĨ提醒īŧˆæŒ¯åŠ¨/é¸ŖåĢīŧ‰ + åēåˆ—åˇ + 业éēĻč¯­ + čĩˇåēŠ + įģŸčŽĄ + 针寚å…ļäģ–įąģåˆĢįš„通įŸĨ发å‡ē提醒īŧˆæŒ¯åŠ¨/é¸ŖåĢīŧ‰ + 小įąŗæ‰‹čĄ¨é’æ˜Ĩį‰ˆ + åœ¨čŽžå¤‡å…čŽ¸įš„情å†ĩ下īŧŒåœ¨é€šįŸĨ标éĸ˜ä¸­æ˜žį¤ēæļˆæ¯éĸ„č§ˆ + 发å‡ē一ä¸Ē提醒åšļåœ¨å°ąå¯æ—ļ间čŋ›å…ĨįĄįœ æ¨Ąåŧã€‚ 在éĸ„厚įš„čĩˇåēŠæ—ļ间īŧŒčĩˇåēŠé—šé’Ÿå°†äŧšå“čĩˇã€‚ + įŸ­äŋĄé€šįŸĨ提醒 + 提醒 + 小įąŗæ™ēčƒŊ手įŽ¯ 7 Pro + 小įąŗæ‰‹čĄ¨ S1 čŋåŠ¨į‰ˆ + 小įąŗæ‰‹čĄ¨åŊŠč‰˛čŋåŠ¨į‰ˆ + Pixoo + å‘čŽžå¤‡å‘é€åē”į”¨é€šįŸĨ + 发送通įŸĨ + č¯ˇå‹ŋ打扰 - åˇ˛åŧ€å¯ + č¯ˇå‹ŋ打扰 - äģ…限闚钟 + č¯ˇå‹ŋ打扰 - äģ…限äŧ˜å…ˆ + æœĒ莞įŊŽ + č¯ˇå‹ŋ打扰 - å…ŗ闭 + äŊŠæˆ´æ–šåŧ + Pebbleīŧˆéž‹æ‰Ŗīŧ‰ + æŦĄčĻį›Žæ ‡ + åŊ“您įš„æ´ģ力å€ŧ在čŋ‡åŽģ 7 å¤Šå†…čžžåˆ° 30、60 或 100 æ—ļīŧŒæ‚¨äŧšæ”ļ到通įŸĨ + æ´ģ力å€ŧ + åŊ“æ‚¨čžžåˆ°åŊ“夊įš„最大æ´ģ力å€ŧæ—ļæ”ļ到通įŸĨ + įĢ™įĢ‹æ—ļ间 + æ´ģ动æ—ļ间 + 手įŽ¯īŧˆč…•å¸Ļīŧ‰ + 项链īŧˆéĸˆå¸Ļīŧ‰ + 每æ—Ĩčŋ›åēĻ + 7夊čŋ›åą• + 手æœē静éŸŗæ¨Ąåŧ + 晎通 / 振动 + 震动 / 静éŸŗ + įēĸįąŗæ™ēčƒŊ手įŽ¯2 + 晎通 / 静éŸŗ + čļŠé‡Žčˇ‘ + 小部äģļå¸ƒåą€ + č¯ĨčŽžå¤‡æ˛Ąæœ‰į”¨äēŽå°éƒ¨äģļåąåš•įš„įŠē闲äŊįŊŽīŧˆäŊįŊŽæ€ģ数īŧš%1$sīŧ‰ + æœĒįŸĨé”ģį‚ŧ - %s + 上į§ģ + 您įĄŽåŽščĻåˆ é™¤â€œ%1$s”吗īŧŸ + åž’æ­Ĩ + 2 ä¸Ē小部äģļ + 删除小部äģļåąåš• + 1 ä¸ĒéĄļ部īŧŒ2 ä¸Ēåē•éƒ¨ + 2 ä¸ĒéĄļ部īŧŒ2 ä¸Ēåē•éƒ¨ + åŋ…éĄģč‡ŗ少有 %1$s ä¸Ēåąåš• + 小部äģļ + 小部äģļåąåš• + 下į§ģ + 1 ä¸Ē小部äģļ + 小部äģļ子įąģ型 + åąåš• %s + č¯ˇé€‰æ‹Šæ‰€æœ‰å°éƒ¨äģļ + 2 ä¸ĒéĄļ部īŧŒ1 ä¸Ēåē•éƒ¨ + æ‘”č§’ + éĢ˜äšéĢ˜ 2023 + éĢ˜äšéĢ˜ 2021 + 拍手äēŽčĩˇåąåš•" + 再æŦĄæ‹æ‰‹å…ŗé—­åąåš•" + éēĻ克éŖŽæŖ€æĩ‹åˆ°é™éŸŗ一æŽĩæ—ļé—´åŽå°†åąåš•å…ŗ闭 + įąŗåŽļ渊æšŋåēĻäŧ æ„Ÿå™¨2 + å¯ŧčˆĒč¯´æ˜Ž + 配įŊŽæ‰‹čĄ¨ä¸Šå¯ŧčˆĒåē”į”¨įš„čĄŒä¸ē + å¯ŧčˆĒåē”į”¨æ”ļ到å¯ŧčˆĒ指äģ¤æ—ļ是åĻč‡Ē动čŋ›å…Ĩ前台 + 栚捎新指äģ¤æŒ¯åŠ¨ + æĨ到前台 + čŽžå¤‡åį§° + æ‰‹čĄ¨æ˜¯åĻåē”č¯Ĩ在每ä¸Ē新įš„或更攚įš„å¯ŧčˆĒ指äģ¤æ—ļ振动īŧˆäģ…åŊ“åē”į”¨į¨‹åēäŊäēŽå‰å°æ—ļīŧ‰ + įēĸįąŗæ‰‹čĄ¨2青æ˜Ĩį‰ˆ + įēĸįąŗæ™ēčƒŊ手įŽ¯Pro + čŋžæŽĨįŠļ态 + Nothing Ear (2) + Nothing Ear (Stick) + čŊģåēĻä¸ģ动降å™Ē + 通透åēĻ + čŖč€€æ‰‹įŽ¯4 + čŖč€€æ‰‹įŽ¯5 + 华ä¸ēč¯­éŸŗ手įŽ¯B6 + 不čĻå–æļˆé€‰ä¸­æ™ēčƒŊå”¤é†’å¤é€‰æĄ†ã€‚ + 厞æ—ļį›‘控您įš„įĄįœ č´¨é‡å’Œå‘ŧå¸æ¨Ąåŧã€‚ +\n分析您įš„įĄįœ æ¨Ąåŧåšļ准įĄŽč¯Šæ–­ 6 į§įĄįœ é—Žéĸ˜ã€‚ + 华ä¸ēæ‰‹čĄ¨GT 3 īŧˆProīŧ‰ + 启į”¨æŽĨåŦį”ĩč¯ + 启į”¨äģŽčŽžå¤‡æŽĨ受į”ĩč¯ + 启į”¨č‡Ē动åŋƒįŽ‡æĩ‹é‡ + 某äē›čŽžå¤‡é”™č¯¯åœ°åŖ°į§°ä¸æ”¯æŒæŸäē›é€‰éĄšã€‚æ— čŽēåĻ‚äŊ•īŧŒæ­¤čŽžįŊŽå¯į”¨äēŽå¯į”¨åŽƒäģŦ。 +\näŊŋį”¨éŖŽé™Šį”ąæ‚¨č‡Ē行æ‰ŋ担。 +\n阅č¯ģįģ´åŸē + åŧēåˆļæ™ēčƒŊæŠĨč­Ļ支持。 +\néŖŽé™Šč‡Ē担 + åŧēåˆļč¯ˇå‹ŋ打扰支持 + 重新č§Ŗ析é”ģį‚ŧ数捎 + åŊ“č“į‰™æ–­åŧ€æ—ļåœ¨čŽžå¤‡ä¸Ščŋ›čĄŒé€šįŸĨ。 + äģ…åŊ“æŠŦč…•å¯į”¨æ—ļæŋ€æ´ģ昞į¤ē + 不äŊŠæˆ´æ—ļ启į”¨č¯ˇå‹ŋ打扰 + 手动 + 全夊 + čŖč€€æ‰‹įŽ¯3 + čŖč€€æ‰‹įŽ¯6 + čŖč€€æ‰‹įŽ¯7 + 华ä¸ē手įŽ¯īŧˆAW70īŧ‰ + čŖč€€æ‰‹įŽ¯6 + čŖč€€æ‰‹įŽ¯7 + čŖč€€æ‰‹įŽ¯8 + 华ä¸ēæ‰‹čĄ¨GT + 华ä¸ē手įŽ¯4īŧˆProīŧ‰ + 华ä¸ēæ‰‹čĄ¨GT 2īŧˆProīŧ‰ + 华ä¸ēæ‰‹čĄ¨GT 2e + åˇĨäŊœæ¨Ąåŧ + 不čĻé€‰ä¸­æ™ēčƒŊå”¤é†’å¤é€‰æĄ†ã€‚ + 华ä¸ē TruSleepâ„ĸ + 攚善įĄįœ į›‘æĩ‹ + 启į”¨æ‹’įģæĨį”ĩ + 启į”¨æ‹’įģæĨč‡ĒčŽžå¤‡įš„å‘ŧåĢ + åŊ“č¯ˇå‹ŋ打扰处äēŽæ´ģ动įŠļ态æ—ļįĻį”¨æŸĨ扞我įš„手æœē + 启į”¨č‡ĒåŠ¨čĄ€æ°§æĩ‹é‡ + åŧēåˆļ选项 + åŧēåˆļæ™ēčƒŊ闹钟 + åŧēåˆļäŊŠæˆ´äŊįŊŽ + åŧēåˆļäŊŠæˆ´äŊįŊŽæ”¯æŒã€‚ +\néŖŽé™Šč‡Ē担 + åŧēåˆļč¯ˇå‹ŋ打扰支持。 +\néŖŽé™Šč‡Ē担 + åŋŊį•Ĩ唤醒启动įŠļ态 + 可čƒŊ有劊äēŽæ­ŖįĄŽįš„įĄįœ æŖ€æĩ‹ã€‚ 在æ—Ĩ常æ´ģåŠ¨č§†å›žä¸­įĢ‹åŗå¯č§ã€‚ + åŋŊį•Ĩ唤醒įģ“束įŠļ态 + 可čƒŊ有劊äēŽæ­ŖįĄŽįš„įĄįœ æŖ€æĩ‹ã€‚ 在æ—Ĩ常æ´ģåŠ¨č§†å›žä¸­įĢ‹åŗå¯č§ã€‚ + čŋ™åĒäŧšåœ¨æŸäē›æ›´æ–°åŽæ‰§čĄŒæŸäē›æ“äŊœ + 向华ä¸ēčŽžå¤‡å‘é€č°ƒč¯•č¯ˇæą‚ + č°ƒč¯•č¯ˇæą‚ + äģ…重新čŋžæŽĨåˆ°åˇ˛čŋžæŽĨįš„čŽžå¤‡ + äģ…重新čŋžæŽĨåˆ°åˇ˛čŋžæŽĨįš„čŽžå¤‡īŧŒč€Œä¸æ˜¯é‡æ–°čŋžæŽĨåˆ°æ‰€æœ‰čŽžå¤‡ + čŽžå¤‡į”ĩæą į”ĩ量čŋ‡äŊŽ + 蜂é¸Ŗ器åŧēåēĻ + 密į åŋ…éĄģä¸ē 4 äŊæ•°å­—īŧŒä¸”åĒčƒŊäŊŋį”¨æ•°å­— + įąŗåŽļ MHO-C303 + į‰ˆæœŦ 1 + 小įąŗæ™ēčƒŊ手įŽ¯ 8 Pro + åšŗéĸ距įĻģ + åčŽŽį‰ˆæœŦ + 小įąŗæ‰‹čĄ¨ S3 + į‰ˆæœŦ 2 + 小įąŗæ‰‹čĄ¨ S1 + į‰ˆæœŦ 3 + 小įąŗæ‰‹čĄ¨ S1 Pro + æ´ģ动äŋĄæ¯ + åĻ‚果您įš„čŽžå¤‡ä¸å“åē” GadgetbridgeīŧŒæ‚¨å¯äģĨå°č¯•åŧēåˆļčŋžæŽĨįąģ型 + æ­Ŗ在上äŧ čĄ¨į›˜â€Ļâ€Ļ + æ­Ŗ在上äŧ čĄ¨į›˜ + 襨į›˜åŽ‰čŖ…åˇ˛åŽŒæˆ + 襨į›˜åŽ‰čŖ…å¤ąč´Ĩ + åŧēåˆļčŋžæŽĨįąģ型 + č‡Ē动 + äŊŽåŠŸč€—č“į‰™ + įģå…¸č“į‰™ + CMF Watch Pro + 155 bpm + 165 bpm + 175 bpm + 185 bpm + 195 bpm + 205 bpm + åŧē力čŋåŠ¨åŋƒįŽ‡č­ĻæŠĨ阈å€ŧ + åŽ¤å†…čˇ‘æ­Ĩ + į™ģåąą + äē¤å‰čŽ­įģƒæœē + č‡Ēį”ąčŽ­įģƒ + čĩ›č‰‡ + 动感单čŊĻ + æĨŧæĸ¯č¸æ­Ĩæœē + åĨčēĢčŋåŠ¨ + įģŧ合åĨčēĢ + 功čƒŊ莭įģƒ + äŊ“åŠ›čŽ­įģƒ + 跆æ‹ŗ道 + čļŠé‡Žčˇ‘ + įŠē手道 + å‡ģ剑 + 剑道 + 单杠 + 双杠 + å†ˇå´ + äē¤å‰čŽ­įģƒ + äģ°å§čĩˇå + åĨčēĢ游戏 + 有氧čŋåŠ¨ + æģšåŠ¨ + äŋ¯å§æ’‘ + 战įģŗ + åŧ•äŊ“向上 + 木æŋ + 标æžĒ + čˇŗčŋœ + 哑铃 + 肚įšŽčˆž + įˆĩåŖĢ舞 + æ‹‰ä¸čˆž + 芭蕾舞 + éŖžé•– + 射įŽ­ + éĒ‘éŠŦ + 攞éŖŽį­ + 摇摆 + æĨŧæĸ¯ + 钓éąŧ + 手éĒ‘č‡Ē行čŊĻ + åŋƒįĩ和čēĢäŊ“ + åĄåˇ´čŋĒ + åĄä¸čŊĻ + 台įƒ + įžŊ毛įƒ + čē˛éŋįƒ + æžŗ大刊äēščļŗįƒ + 匚克įƒ + æ›˛æŖįƒ + 射å‡ģ + å¸†čˆščŋåŠ¨ + æģ‘æ°´ + į´ĸå°ŧ WI-SP600N + æ­Ļ术 + å˛å¯†æ–¯æœē + įĩæ´ģ性 + į”°åž„ + 在一æŽĩæ—ļ间内įĻį”¨čĄĨ充水分č­Ļ告 + čˇŗéĢ˜ + å…ļäģ–čˆžčšˆ + å¤Ē极 + éŖžį›˜čŋåŠ¨ + čšĻåēŠ + 冰įƒ + čŊŽæģ‘ + å‘ŧå•Ļ圈 + 垒įƒ + æēœå†° + 冰åŖļ + é›Ē上čŋåŠ¨ + 打įŒŽ + æˆˇå¤–æ•Ŗæ­Ĩ + čļŠé‡Žæģ‘é›Ē + æģ‘æŋčŋåŠ¨ + æ”€å˛Š + čŖ耀 MagicWatch 2 + įēĸįąŗæ‰‹čĄ¨2 \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 9f0cb3fcb..f416de506 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -1,5 +1,9 @@ + + + + @string/pref_theme_system @string/pref_theme_light @@ -157,6 +161,17 @@ buttons_on_right + + @string/wearmode_band + @string/wearmode_pebble + @string/wearmode_necklace + + + band + pebble + necklace + + @string/horizontal @string/vertical @@ -189,6 +204,17 @@ @string/p_alarm_clock + + @string/silent_mode_normal_vibrate + @string/silent_mode_normal_silent + @string/silent_mode_vibrate_silent + + + normal_vibrate + normal_silent + vibrate_silent + + Pebble Health Misfit @@ -271,6 +297,11 @@ @string/mi2_dnd_automatic @string/mi2_dnd_scheduled + + @string/mi2_dnd_off + @string/dnd_all_day + @string/mi2_dnd_scheduled + @string/p_off @string/p_automatic @@ -1001,6 +1032,7 @@ @string/menuitem_findphone @string/menuitem_eject_water @string/menuitem_headphone + @string/menuitem_buzzer_intensity @@ -1020,6 +1052,7 @@ findphone eject_water headphone + buzzer_intensity @@ -1168,6 +1201,237 @@ indoor_ice_skating + + @string/activity_type_indoor_running + @string/activity_type_outdoor_running + @string/activity_type_outdoor_walking + @string/activity_type_indoor_walking + @string/activity_type_outdoor_cycling + @string/activity_type_indoor_cycling + @string/activity_type_mountain_hike + @string/activity_type_hiking + @string/activity_type_cross_trainer + @string/activity_type_free_training + @string/activity_type_strength_training + @string/activity_type_yoga + @string/activity_type_boxing + @string/activity_type_rower + @string/activity_type_dynamic_cycle + @string/activity_type_stair_stepper + @string/activity_type_treadmill + @string/activity_type_hiit + @string/activity_type_fitness_exercises + @string/activity_type_jump_roping + @string/activity_type_pilates + @string/activity_type_crossfit + @string/activity_type_functional_training + @string/activity_type_physical_training + @string/activity_type_taekwondo + @string/activity_type_cross_country_running + @string/activity_type_karate + @string/activity_type_fencing + @string/activity_type_core_training + @string/activity_type_kendo + @string/activity_type_horizontal_bar + @string/activity_type_parallel_bar + @string/activity_type_cooldown + @string/activity_type_cross_training + @string/activity_type_sit_ups + @string/activity_type_fitness_gaming + @string/activity_type_aerobic_exercise + @string/activity_type_rolling + @string/activity_type_flexibility + @string/activity_type_gymnastics + @string/activity_type_track_and_field + @string/activity_type_push_ups + @string/activity_type_battle_rope + @string/activity_type_smith_machine + @string/activity_type_pull_ups + @string/activity_type_plank + @string/activity_type_javelin + @string/activity_type_long_jump + @string/activity_type_high_jump + @string/activity_type_trampoline + @string/activity_type_dumbbell + @string/activity_type_belly_dance + @string/activity_type_jazz_dance + @string/activity_type_latin_dance + @string/activity_type_ballet + @string/activity_type_street_dance + @string/activity_type_zumba + @string/activity_type_other_dance + @string/activity_type_roller_skating + @string/activity_type_martial_arts + @string/activity_type_tai_chi + @string/activity_type_hula_hooping + @string/activity_type_disc_sports + @string/activity_type_darts + @string/activity_type_archery + @string/activity_type_horse_riding + @string/activity_type_kite_flying + @string/activity_type_swing + @string/activity_type_stairs + @string/activity_type_fishing + @string/activity_type_hand_cycling + @string/activity_type_mind_and_body + @string/activity_type_wrestling + @string/activity_type_kabaddi + @string/activity_type_karting + @string/activity_type_badminton + @string/activity_type_table_tennis + @string/activity_type_tennis + @string/activity_type_billiards + @string/activity_type_bowling + @string/activity_type_volleyball + @string/activity_type_shuttlecock + @string/activity_type_handball + @string/activity_type_baseball + @string/activity_type_softball + @string/activity_type_cricket + @string/activity_type_rugby + @string/activity_type_hockey + @string/activity_type_squash + @string/activity_type_dodgeball + @string/activity_type_soccer + @string/activity_type_basketball + @string/activity_type_australian_football + @string/activity_type_golf + @string/activity_type_pickleball + @string/activity_type_lacross + @string/activity_type_shot + @string/activity_type_sailing + @string/activity_type_surfing + @string/activity_type_jet_skiing + @string/activity_type_skating + @string/activity_type_ice_hockey + @string/activity_type_curling + @string/activity_type_snowboarding + @string/activity_type_cross_country_skiing + @string/activity_type_snow_sports + @string/activity_type_skiing + @string/activity_type_skateboarding + @string/activity_type_rock_climbing + @string/activity_type_hunting + + + + indoor_running + outdoor_running + outdoor_walking + indoor_walking + outdoor_cycling + indoor_cycling + mountain_hike + hiking + cross_trainer + free_training + strength_training + yoga + boxing + rower + dynamic_cycle + stair_stepper + treadmill + hiit + fitness_exercises + jump_roping + pilates + crossfit + functional_training + physical_training + taekwondo + cross_country_running + karate + fencing + core_training + kendo + horizontal_bar + parallel_bar + cooldown + cross_training + sit_ups + fitness_gaming + aerobic_exercise + rolling + flexibility + gymnastics + track_and_field + push_ups + battle_rope + smith_machine + pull_ups + plank + javelin + long_jump + high_jump + trampoline + dumbbell + belly_dance + jazz_dance + latin_dance + ballet + street_dance + zumba + other_dance + roller_skating + martial_arts + tai_chi + hula_hooping + disc_sports + darts + archery + horse_riding + kite_flying + swing + stairs + fishing + hand_cycling + mind_and_body + wrestling + kabaddi + karting + badminton + table_tennis + tennis + billiards + bowling + volleyball + shuttlecock + handball + baseball + softball + cricket + rugby + hockey + squash + dodgeball + soccer + basketball + australian_football + golf + pickleball + lacross + shot + sailing + surfing + jet_skiing + skating + ice_hockey + curling + snowboarding + cross_country_skiing + snow_sports + skiing + skateboarding + rock_climbing + hunting + + + + indoor_run + outdoor_run + + @string/activity_type_outdoor_running @string/activity_type_hiking @@ -2085,6 +2349,16 @@ @string/timeformat_am_pm + + active_time + standing_time + + + + @string/active_time + @string/standing_time + + @string/p_timeformat_auto @string/p_timeformat_24h @@ -2150,6 +2424,10 @@ @string/japanese @string/korean @string/hebrew + @string/danish + @string/norwegian_bokmal + @string/romanian + @string/swedish @@ -2184,6 +2462,10 @@ ja_JP ko_KO he_IL + da_DK + nb_NO + ro_RO + sv_SE @@ -2338,6 +2620,26 @@ 150 + + @string/off + @string/heartrate_bpm_155 + @string/heartrate_bpm_165 + @string/heartrate_bpm_175 + @string/heartrate_bpm_185 + @string/heartrate_bpm_195 + @string/heartrate_bpm_205 + + + + 0 + 155 + 165 + 175 + 185 + 195 + 205 + + @string/off @string/heartrate_bpm_40 @@ -2546,7 +2848,6 @@ - @string/pref_button_action_disabled @string/pref_media_play @string/pref_media_pause @string/pref_media_playpause @@ -2554,10 +2855,13 @@ @string/pref_device_action_fitness_app_control_start @string/pref_device_action_fitness_app_control_stop @string/pref_device_action_fitness_app_control_toggle + @string/pref_device_action_dnd_off + @string/pref_device_action_dnd_priority + @string/pref_device_action_dnd_alarms + @string/pref_device_action_dnd_on - @string/pref_button_action_disabled_value @string/pref_media_play_value @string/pref_media_pause_value @string/pref_media_playpause_value @@ -2565,6 +2869,10 @@ @string/pref_device_action_fitness_app_control_start_value @string/pref_device_action_fitness_app_control_stop_value @string/pref_device_action_fitness_app_control_toggle_value + @string/pref_device_action_dnd_off_value + @string/pref_device_action_dnd_priority_value + @string/pref_device_action_dnd_alarms_value + @string/pref_device_action_dnd_on_value @@ -2770,8 +3078,15 @@ 2 3 - - + + + @string/prefs_active_noise_cancelling + @string/prefs_active_noise_cancelling_light + @string/prefs_active_noise_cancelling_transparency + @string/off + + + anc anc-light transparency @@ -2955,6 +3270,20 @@ ambient_sound + + @string/automatic + @string/sony_protocol_v1 + @string/sony_protocol_v2 + @string/sony_protocol_v3 + + + + auto + v1 + v2 + v3 + + @string/sony_sound_position_off @string/sony_sound_position_front @@ -3116,6 +3445,29 @@ de.dennisguse.opentracks.nightly + + @string/automatic + net.osmand.plus + net.osmand + net.osmand.dev + + + autodetect + net.osmand.plus + net.osmand + net.osmand.dev + + + + @string/automatic + @string/manual + + + + auto + manual + + @string/arabic @string/bengali @@ -3139,6 +3491,7 @@ @string/scandinavian @string/turkish @string/ukranian + @string/hungarian @@ -3164,6 +3517,7 @@ scandinavian turkish ukranian + hungarian @@ -3478,4 +3832,14 @@ ignore + + @string/pref_force_connection_type_auto + @string/pref_force_connection_type_ble + @string/pref_force_connection_type_bt_classic + + + @string/pref_force_connection_type_auto_value + @string/pref_force_connection_type_ble_value + @string/pref_force_connection_type_bt_classic_value + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bfceeb221..6829778d7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -203,7 +203,9 @@ Time Workout Equalizer - Connect to Gadgetbridge device when Bluetooth is turned on + Connect to Gadgetbridge device(s) when Bluetooth is turned on + Reconnect only to connected devices + Reconnect only to connected devices, instead of reconnecting to all devices Start automatically Reconnect automatically Broadcast Media Button Intents Directly @@ -332,10 +334,24 @@ Send calendar events to the timeline Show device specific notification icon Show a device specific Android notification icon instead the Gadgetbridge icon when connected + Show a preview of the message in the title + Shows a preview of the message in the title of a notification as allowed by the device + Alert for calendar notifications + Alert (vibrate/beep) for calendar notifications + Alert for incoming calls + Alert (vibrate/beep) for incoming calls + Alert for email notifications + Alert (vibrate/beep) for email notifications + Alert for SMS notifications + Alert (vibrate/beep) for SMS (text message) notifications + Alert for "other" notifications + Alert (vibrate/beep) for notifications in the "other" category Autoremove dismissed notifications Notifications are automatically removed from the device when dismissed from the phone Screen On on Notifications Turn on the band\'s screen when a notification arrives + Send notifications + Send app notifications to the device Privacy mode Normal notifications Shift the notification text off-screen @@ -394,6 +410,7 @@ Units Time format Screen on duration + Secondary goal All day heart rate measurement HPlus/Makibes settings @@ -478,6 +495,10 @@ Beep once Beep twice Vibrate and beep once + + Clap hands to turn up screen" + Clapping again will turn off the screen" + The screen will turn off after the microphone has detected silence for a while Device specific settings Auth Key @@ -604,6 +625,9 @@ Vertical Buttons on left Buttons on right + Band (wristband) + Pebble (shoe buckle) + Necklace (neck strap) No valid user data given, using dummy user data for now. When your Mi Band vibrates and blinks, tap it a few times in a row. Install @@ -635,6 +659,7 @@ From %1$s to %2$s Wearing left or right? Wearing direction + Wearing mode Vibration profile Default Staccato @@ -687,6 +712,12 @@ 140 bpm 145 bpm 150 bpm + 155 bpm + 165 bpm + 175 bpm + 185 bpm + 195 bpm + 205 bpm 80% 85% 90% @@ -760,6 +791,7 @@ Vibrate the band when the heart rate is over a threshold, without any obvious physical activity in the last 10 minutes. This feature is experimental, and was not extensively tested. Heart rate alert threshold High heart rate alert threshold + High activity heart rate alert threshold Low heart rate alert threshold Stress monitoring Monitor stress level while resting @@ -833,6 +865,7 @@ Problem with the firmware transfer. DO NOT REBOOT your Mi Band! Problem with the firmware metadata transfer The device does not have enough free space + The device battery is too low Firmware installation complete Firmware installation complete, rebooting deviceâ€Ļ Firmware flashing failed @@ -918,6 +951,7 @@ Time Disconnect notification + Notification on device when disconnected from BT. Button actions Specify button press actions Button press count @@ -943,21 +977,30 @@ The band will vibrate when you have been inactive for a while Inactivity threshold (in minutes) Disable inactivity warnings for a time interval + Disable hydration warnings for a time interval Heart Rate Monitoring Configure heart rate monitoring + Phone Silent Mode + Normal / Vibrate + Normal / Silent + Vibrate / Silent Always On Display Style follows Watchface Style Keep the band\'s display always on + Device name Password Lock the band with a password when removed from the wrist Password Enabled The password must have 4 digits, using numbers 1 to 4 + The password must have 4 digits, using only numbers The password must have 6 digits, using only numbers Configure heart rate monitoring and alert thresholds Start time End time Activate display upon lift during Do Not Disturb + Only if activate display upon lift enabled + Do not disturb when not wearing Band screen unlock" Swipe up to unlock the band\'s screen Night mode @@ -981,6 +1024,7 @@ The band will vibrate if your phone disconnects from the band Interface language Automatic + Manual Simplified Chinese Traditional Chinese English @@ -1032,6 +1076,7 @@ Hebrew Swedish Czech + Danish About to transfer data since %1$s Waiting for reconnect About you @@ -1085,6 +1130,8 @@ Daily target: active time in minutes Daily target: standing time in minutes Daily target: fat burn time in minutes + Active time + Standing time Store raw record in the database Stores the data \"as is\", increasing the database usage to allow for later interpretation. Data management @@ -1175,6 +1222,7 @@ At sunset Automatic (sleep detection) Scheduled (time interval) + All day Duration Attempting to pair with %1$s Bonding with %1$s failed immediately. @@ -1197,7 +1245,83 @@ Device not worn Running Outdoor Running + Indoor Running + Mountain Hike + Cross trainer + Free training + Rower + Dynamic cycle + Stair stepper + Fitness exercises + Crossfit + Functional training + Physical training + Taekwondo + Cross country running + Karate + Fencing + Kendo + Horizontal bar + Parallel bar + Cooldown + Cross training + Sit ups + Fitness gaming + Aerobic exercise + Rolling + Flexibility + Track and field + Push ups + Battle rope + Smith machine + Pull ups + Plank + Javelin + Long jump + High jump + Trampoline + Dumbbell + Belly dance + Jazz dance + Latin dance + Ballet + Other dance + Roller skating + Martial arts + Tai chi + Hula hooping + Disc sports + Darts + Archery + Horse riding + Kite Flying + Swing + Stairs + Fishing + Hand cycling + Mind and body + Kabaddi + Karting + Billiards + Shuttlecock + Softball + Dodgeball + Australian football + Pickleball + Lacross + Shot + Sailing + Jet skiing + Skating + Ice hockey + Curling + Cross country skiing + Snow sports + Skateboarding + Rock climbing + Hunting Walking + Outdoor Walking Indoor Walking Surfing Windsurfing @@ -1254,6 +1378,9 @@ Ice Skating Golfing Other + Trekking + Trail run + Wrestling Unknown activity Sport Activities Sport Activity Detail @@ -1296,6 +1423,9 @@ Mi Band 5 Mi Band 6 Xiaomi Smart Band 7 + Xiaomi Smart Band 7 Pro + Xiaomi Smart Band 8 + Xiaomi Smart Band 8 Pro Amazfit Balance Amazfit Active Amazfit Active Edge @@ -1365,6 +1495,8 @@ iTag BFH-16 Mijia Smart Clock + Mijia Temperature and Humidity Sensor 2 + Mijia MHO-C303 Makibes HR3 Bangle.js TLW64 @@ -1373,6 +1505,8 @@ Wasp-os SMA-Q2 OSS FitPro + ColaCao 2021 + ColaCao 2023 Domyos T540 Sony WH-1000XM2 Sony WH-1000XM3 @@ -1382,9 +1516,32 @@ Sony WF-1000XM3 Sony WF-1000XM4 Sony WF-1000XM5 + Sony WI-SP600N Sony LinkBuds S Binary sensor + Honor Band 3 + Honor Band 4 + Honor Band 5 + Honor Band 6 + Honor Band 7 + Honor MagicWatch 2 + Huawei Band (AW70) + Huawei Band 6 + Huawei Band 7 + Huawei Band 8 + Huawei Watch GT + Huawei Band 4 (Pro) + Huawei Watch GT 2 (Pro) + Huawei Watch GT 2e + Huawei Talk Band B6 + Huawei Watch GT 3 (Pro) Femometer Vinca II + Xiaomi Watch Lite + Redmi Watch 3 Active + Redmi Smart Band 2 + Redmi Watch 2 + Redmi Watch 2 Lite + Redmi Smart Band Pro Choose export location General High-priority @@ -1476,6 +1633,7 @@ Lockscreen Eject Water Headphone + Buzzer Intensity Unknown (%s) [UNSUPPORTED] %s Red Fantasy @@ -1659,6 +1817,7 @@ Downhill Uphill distance Downhill distance + Flat distance Elevation gain Elevation loss Maximum @@ -1916,6 +2075,9 @@ Take measurements during sleep Frequency of measurements Nothing Ear (1) + Nothing Ear (2) + Nothing Ear (Stick) + CMF Watch Pro Galaxy Buds Galaxy Buds Live Galaxy Buds Pro @@ -1956,6 +2118,8 @@ Ambient Mode Ambient Sound Options Active Noise Cancelling + Light Active Noise Cancelling + Transparency Active Noise Cancelling Level High Low @@ -2009,8 +2173,8 @@ Mild Moderate High - PAI Total - Day PAI increase + Total + Day increase Mode Off Noise Cancelling @@ -2106,6 +2270,10 @@ Ambient Sound, Off Quick Access (Double Tap) Quick Access (Triple Tap) + Version 1 + Version 2 + Version 3 + Protocol Version Auto Brightness Adjust screen brightness according to ambient light Screen Brightness @@ -2142,6 +2310,10 @@ Fitness App Tracking Stop Toggle Fitness App Tracking GPS Location Listener Stop + Do not disturb - Off + Do not disturb - Priority only + Do not disturb - Alarms only + Do not disturb - On ###m @@ -2151,6 +2323,21 @@ ###ft + Work Mode + Do not uncheck smart wakeup checkbox. + Do not check smart wakeup checkbox. + HUAWEI TruSleep ™ + Monitor your sleep quality and breathing pattern in real time.\nAnalyze your sleep patterns and accurately diagnose 6 types of sleeping problems. + Improved sleep monitoring + Activity recognition settings + recognize running + recognize biking + recognize walking + recognize rowing + none + ask + auto + Menu Some buttons cannot be configured because their functions are hard-coded in the watch firmware.\n\nWarning: long-pressing the upper button when a watchface from the official Fossil app is installed will also toggle between showing/hiding widgets. OpenTracks package name @@ -2160,6 +2347,31 @@ pre-setting position to %s Light up on new notification + Enable accepting calls + Enable accepting calls from the device + Enable rejecting calls + Enable rejecting calls from the device + + Disable find my phone when do not disturb is active + + Enable automatic heartrate measuring + Enable automatic SpO2 measuring + + Force options + Some devices falsely claim not to have support for some options. This settings can be used to enable them anyway.\nUSE AT YOUR OWN RISK\nRead the wiki + Force smart alarm + Force smart alarms support.\nUSE AT YOUR OWN RISK + Force wear location + Force wear location support.\nUSE AT YOUR OWN RISK + Force Do Not Disturb support + Force Do Not Disturb support.\nUSE AT YOUR OWN RISK + Ignore wakeup start status + May help with proper sleep detection. Visible immediately in the daily activities view. + Ignore wakeup end status + May help with proper sleep detection. Visible immediately in the daily activities view. + "Reparse workout data" + "This will only do something after certain updates" + no devices connected %d devices connected Set parent folder @@ -2214,6 +2426,8 @@ Turbo Speed Lights Blinking + Send a debug request to Huawei device + Debug request AsteroidOS SoFlow SO6 Lock @@ -2386,7 +2600,6 @@ Siren Short No LED - Temperature scale Select whether device uses Celsius or Fahrenheit scale. Celsius @@ -2397,6 +2610,73 @@ Ignore (silence) Call rejection method Which action is taken when an incoming call is rejected from the watch + App name in notification + Prefix notification title with name of source application + OsmAnd package name + Used for selecting the version of OsmAnd to connect to + Navigation preferences + Navigation apps + OsmAnd(+) + Google Maps + Serial Number + Stats + Running + Alerts + Focus + Bedtime + Wake Up + Sleep Mode Schedule + Send a reminder and enter sleep mode at bedtime. At the scheduled wake-up time, the wake-up alarm will sound. + Xiaomi Watch S1 + Xiaomi Watch S3 + Xiaomi Watch S1 Active + Xiaomi Watch S1 Pro + Mi Watch Color Sport + Pixoo + Not set + Vitality Score + 7-day progress + Get a notification when your vitality score reaches 30, 60 or 100 in the past 7 days + Daily progress + Get a notification when you reached the maximum number of vitality points for the day + Widget + Widget Screen + Delete widget screen + Are you sure you want to delete \'%1$s\'? + The device has no free slots for widget screens (total slots: %1$s) + There must be a minimum of %1$s screens + 1 top, 2 bottom + 2 top, 1 bottom + 2 top, 2 bottom + Widget layout + Widget Subtype + Screen %s + 1 widget + 2 widgets + Move up + Move down + Please select all widgets + Unknown workout - %s + Navigation instructions + Configure on-watch navigation app behavior + Come to foreground + Whether the navigation app should automatically come to the foreground when it receives a navigation instruction + Vibrate on new instruction + Whether the watch should vibrate on every new or changed navigation instruction (only when the app is in the foreground) + Connection Status + Uploading watchfaceâ€Ļ + Uploading watchface + Watchface installation completed + Watchface installation failed + Force connection type + You may try forcing the connection type in case your device does not respond to Gadgetbridge + Automatic + Bluetooth LE + Bluetooth Classic + BOTH + BLE + BT_CLASSIC + Activity info Waiting for device scan Reconnect by BLE scan Wait for device scan instead of blind connection attempts diff --git a/app/src/main/res/values/values.xml b/app/src/main/res/values/values.xml index 3cddf520d..d1d4187bb 100644 --- a/app/src/main/res/values/values.xml +++ b/app/src/main/res/values/values.xml @@ -125,6 +125,10 @@ FITNESS_CONTROL_START FITNESS_CONTROL_STOP FITNESS_CONTROL_TOGGLE + DO_NOT_DISTURB_OFF + DO_NOT_DISTURB_PRIORITY + DO_NOT_DISTURB_ALARMS + DO_NOT_DISTURB_ON system light diff --git a/app/src/main/res/xml/changelog_master.xml b/app/src/main/res/xml/changelog_master.xml index 84dbd2343..0b3fc33d4 100644 --- a/app/src/main/res/xml/changelog_master.xml +++ b/app/src/main/res/xml/changelog_master.xml @@ -1,5 +1,140 @@ + + Initial support for Honor Magic Watch 2 + Initial support for Mijia MHO-C303 + Initial support for Nothing CMF Watch Pro + Initial support for Sony WI-SP600N + Experimental support for Redmi Watch 2 + Experimental support for Xiaomi Smart Band 8 Pro + Experimental support for Xiaomi Watch S1 Pro + Experimental support for Xiaomi Watch S1 + Experimental support for Xiaomi Watch S3 + Galaxy Buds2 Pro: Fix recognition of some versions + Huawei Watch GT 2: Fix pairing + Redmi Smart Band Pro: Fix password digits + Pebble: Fix app configuration page + Pebble 2: Fix pairing issue + PineTime: Fix weather forecast on InfiniTime's new simple weather + Xiaomi: Fix sleep sometimes extending past the wakeup time + Xiaomi: Request battery level and charging state periodically + Xiaomi: Fix sleep stage parsing for some devices + Zepp OS: Improve device discovery + Zepp OS: Fix weather not working on some devices + Zepp OS: Prevent crash when installing large firmware updates + Fix sport activity summary group order + Fix reconnection to devices failing occasionally + + + Initial support for Honor Band 3,4,5,6 + Initial support for Huawei Band 4, 4 Pro, 6, 7, 3e, 4e + Initial support for Huawei Talk Band B6 + Initial support for Huawei Watch GT, GT 2 + Initial support for Mijia LYWSD03MMC + Initial support for Nothing Ear (2) + Initial support for Nothing Ear (Stick) + Experimental support for Honor Band 7 + Experimental support for Redmi Watch 2 Lite + Experimental support for Redmi Smart Band Pro + Casio GBX100: Add support for snooze alarm + Fossil/Skagen Hybrids: Update navigationApp to 1.1 + Huami: Fetch SpO2 on devices that support it + Pebble: Attempt to fix app configuration webview + PineTime: Add support for InfiniTime's new simple weather + PineTime: Fix freeze and reboot when upgrading firmware + Pixoo: Enable sending images (non-persistent) + Pixoo: Get and send alarms + Pixoo: Set custom device name + Pixoo: support "clap hands to turn off screen" and "sleep after silence" settings + Xiaomi: Improve activity and workout parsing + Xiaomi: Improve stability and fix some crashes + Xiaomi: Improve weather + Xiaomi: Parse sleep stages + Add a notifications channel for connection status notifications + Improve automatic connection to all or previous devices + Fix devices sometimes staying stuck in a "Connecting" state + Map some missing Google Maps navigation actions + + + Initial support for Amazfit Balance + Initial support for Amazfit Active + Initial support for ColaCao 2021 + Initial support for ColaCao 2023 + Initial support for Femometer Vinca II + Initial support for Mijia LYWSD02MMC variant + Initial support for Sony Wena 3 + Experimental support for Divoom Pixoo + Experimental support for Sony WF-1000XM5 + Experimental support for Amazfit Active Edge + Experimental support for Mi Band 7 Pro (Xiaomi Smart Band 7 Pro) + Experimental support for Mi Band 8 (Xiaomi Smart Band 8) + Experimental support for Mi Watch Lite + Experimental support for Mi Watch Color Sport + Experimental support for Redmi Smart Band 2 + Experimental support for Redmi Watch 3 Active + Experimental support for Xiaomi Watch S1 Active + Amazfit Band 7: Add alexa menu entries + Amazfit GTR 3 Pro: Fix firmware and watchface upload + Amazfit T-Rex: Fix activity summary parsing + Amazfit T-Rex Pro: Add activate display on lift sensitivity + AsteroidOS: Add more supported watch models + AsteroidOS: Fix media info + AsteroidOS: Fix notification dismissal + Bangle.js: Add loyalty cards integration with Catima + Bangle.js: Ensure SMS messages have src field set to "SMS Message" + Bangle.js: Fix GPS speed + Bangle.js: Improve handling of chinese characters + Bangle.js: Lower threshold for low battery warning + Bangle.js: Recover from device initialization failure + Casio GBX100/GBD-200: Fix first connect + Casio GB5600/6900/STB-1000: Fix pairing + Casio GDB-200: Fix notification timestamp + Casio GDB-200: Fixed notification categories and default category + Casio GDB-200: Allow preview of notification message alongside title + Casio GDB-200: Fixed find my phone feature + Intent API: Add debug action for test new function + Fossil/Skagen Hybrids: Add new navigation app + Fossil/Skagen Hybrids: Allow configuring call rejection method + Fossil/Skagen Hybrids: Fix some preference crashes on the nightly + Fossil/Skagen Hybrids: Reduce toasts on release builds + Fossil/Skagen Hybrids: Show device specific settings in more logical order + Huami: Toggle phone silent mode from band + Message privacy: Add mode Hide only body + Mijia LYWSD02: Add battery + Mijia LYWSD02: Add low battery notification + Mijia LYWSD02: Set temperature unit + Mijia LYWSD02: Fix battery drain while connected + PineTime: Display app name for VoIP app calls + PineTime: Honor Sync time setting on connect + PineTime: Improve notification handling + PineTime: Reduce weather memory usage + Withings Steel HR: Fix crash when calibrating hands on the nightly + Zepp OS: Add blood oxygen graph + Zepp OS: Add workout codes for hiking and outdoor swimming + Zepp OS: Allow disabling app notifications per device + Zepp OS: Attempt to fix activity fetch operation getting stuck + Zepp OS: Display swimming activity data + Zepp OS: Fix health settings on older Zepp OS versions + Zepp OS: Fix setting of unknown button press apps + Zepp OS: Fix sunrise and moon dates being off by local time + UTC offset + Zepp OS: Map hiking, outdoor swimming, climbing and table tennis activity types + Zepp OS: Toggle phone silent mode from band + Add transliteration for Latvian, Hungarian, Common Symbols + Allow multiple device actions to be triggered for the same event + Allow toggling DND through device actions + Autodetect OsmAnd package name and make it configurable + Improve ASCII transliterator + Make GMaps navigation handler follow the "navigation forwarding" setting + Support selecting enabled navigation apps + Allow ignore notifications from work profile apps + Display alias in low battery notification + Fix crash when pairing current device as companion + Fix emoji when a transliterator is enabled + Fix UV Index and rain probability for some weather apps + Improve device discovery stability and fix freezes + Improve Telegram and COL Reminder notifications + Replace old-style preference switch with Material 3 switch + Amazfit GTR Mini: Mark as not experimental Bangle.js: Improve file downloads diff --git a/app/src/main/res/xml/devicesettings_activity_info_header.xml b/app/src/main/res/xml/devicesettings_activity_info_header.xml new file mode 100644 index 000000000..5b281033c --- /dev/null +++ b/app/src/main/res/xml/devicesettings_activity_info_header.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/devicesettings_allow_accept_reject_calls.xml b/app/src/main/res/xml/devicesettings_allow_accept_reject_calls.xml new file mode 100644 index 000000000..f233e9471 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_allow_accept_reject_calls.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/devicesettings_autoremove_notifications.xml b/app/src/main/res/xml/devicesettings_autoremove_notifications.xml index 0ecb6d6aa..682b7156c 100644 --- a/app/src/main/res/xml/devicesettings_autoremove_notifications.xml +++ b/app/src/main/res/xml/devicesettings_autoremove_notifications.xml @@ -6,4 +6,4 @@ android:key="autoremove_notifications" android:summary="@string/pref_summary_autoremove_notifications" android:title="@string/pref_title_autoremove_notifications" /> - \ No newline at end of file + diff --git a/app/src/main/res/xml/devicesettings_canned_dismisscall_16.xml b/app/src/main/res/xml/devicesettings_canned_dismisscall_16.xml index 8722b5640..5436114d5 100644 --- a/app/src/main/res/xml/devicesettings_canned_dismisscall_16.xml +++ b/app/src/main/res/xml/devicesettings_canned_dismisscall_16.xml @@ -13,6 +13,11 @@ android:summary="@string/pref_summary_canned_messages_set" android:title="@string/pref_title_canned_messages_set" /> + + diff --git a/app/src/main/res/xml/devicesettings_canned_reply_16.xml b/app/src/main/res/xml/devicesettings_canned_reply_16.xml index 50e2455d8..5d1790f78 100644 --- a/app/src/main/res/xml/devicesettings_canned_reply_16.xml +++ b/app/src/main/res/xml/devicesettings_canned_reply_16.xml @@ -20,6 +20,11 @@ android:title="@string/pref_title_canned_reply_suffix" app:useSimpleSummaryProvider="true" /> + + diff --git a/app/src/main/res/xml/devicesettings_casio_alert.xml b/app/src/main/res/xml/devicesettings_casio_alert.xml new file mode 100644 index 000000000..c5327fe6a --- /dev/null +++ b/app/src/main/res/xml/devicesettings_casio_alert.xml @@ -0,0 +1,33 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/devicesettings_device_actions.xml b/app/src/main/res/xml/devicesettings_device_actions.xml index 39ca0a2d4..db353cfc6 100644 --- a/app/src/main/res/xml/devicesettings_device_actions.xml +++ b/app/src/main/res/xml/devicesettings_device_actions.xml @@ -12,12 +12,11 @@ android:title="@string/prefs_events_forwarding_fellsleep" android:icon="@drawable/ic_nights_stay"> - - - - - + + + diff --git a/app/src/main/res/xml/devicesettings_disable_find_phone_with_dnd.xml b/app/src/main/res/xml/devicesettings_disable_find_phone_with_dnd.xml new file mode 100644 index 000000000..6dc02369b --- /dev/null +++ b/app/src/main/res/xml/devicesettings_disable_find_phone_with_dnd.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/devicesettings_donotdisturb_allday_liftwirst.xml b/app/src/main/res/xml/devicesettings_donotdisturb_allday_liftwirst.xml new file mode 100644 index 000000000..1109a700c --- /dev/null +++ b/app/src/main/res/xml/devicesettings_donotdisturb_allday_liftwirst.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/devicesettings_force_connection_type.xml b/app/src/main/res/xml/devicesettings_force_connection_type.xml new file mode 100644 index 000000000..3b584e03f --- /dev/null +++ b/app/src/main/res/xml/devicesettings_force_connection_type.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/xml/devicesettings_force_options.xml b/app/src/main/res/xml/devicesettings_force_options.xml new file mode 100644 index 000000000..8b1e3ba89 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_force_options.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/devicesettings_fossilhybridhr_all_fw.xml b/app/src/main/res/xml/devicesettings_fossilhybridhr_all_fw.xml index c05326e6c..13b70bb60 100644 --- a/app/src/main/res/xml/devicesettings_fossilhybridhr_all_fw.xml +++ b/app/src/main/res/xml/devicesettings_fossilhybridhr_all_fw.xml @@ -131,4 +131,25 @@ android:targetClass="nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.CalibrationActivity" /> + + + + + + + diff --git a/app/src/main/res/xml/devicesettings_goal_secondary.xml b/app/src/main/res/xml/devicesettings_goal_secondary.xml new file mode 100644 index 000000000..bfb1c47d0 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_goal_secondary.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/xml/devicesettings_heartrate_automatic_enable.xml b/app/src/main/res/xml/devicesettings_heartrate_automatic_enable.xml new file mode 100644 index 000000000..8bed9f4a5 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_heartrate_automatic_enable.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/app/src/main/res/xml/devicesettings_heartrate_sleep_alert_activity_stress_spo2.xml b/app/src/main/res/xml/devicesettings_heartrate_sleep_alert_activity_stress_spo2.xml index b2a84fc37..2cbe5135c 100644 --- a/app/src/main/res/xml/devicesettings_heartrate_sleep_alert_activity_stress_spo2.xml +++ b/app/src/main/res/xml/devicesettings_heartrate_sleep_alert_activity_stress_spo2.xml @@ -70,6 +70,15 @@ android:key="heartrate_alert_low_threshold" android:summary="%s" android:title="@string/prefs_heartrate_alert_low_threshold" /> + + diff --git a/app/src/main/res/xml/devicesettings_huawei.xml b/app/src/main/res/xml/devicesettings_huawei.xml new file mode 100644 index 000000000..d0c8a8011 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_huawei.xml @@ -0,0 +1,241 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/devicesettings_hydration_reminder_dnd.xml b/app/src/main/res/xml/devicesettings_hydration_reminder_dnd.xml new file mode 100644 index 000000000..a0295022e --- /dev/null +++ b/app/src/main/res/xml/devicesettings_hydration_reminder_dnd.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/devicesettings_inactivity_sheduled.xml b/app/src/main/res/xml/devicesettings_inactivity_sheduled.xml new file mode 100644 index 000000000..bc98c8ad8 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_inactivity_sheduled.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/devicesettings_nothing_ear1.xml b/app/src/main/res/xml/devicesettings_nothing_ear1.xml index 746624935..05dd62534 100644 --- a/app/src/main/res/xml/devicesettings_nothing_ear1.xml +++ b/app/src/main/res/xml/devicesettings_nothing_ear1.xml @@ -8,8 +8,8 @@ android:title="@string/nothing_prefs_inear_title" /> diff --git a/app/src/main/res/xml/devicesettings_phone_silent_mode.xml b/app/src/main/res/xml/devicesettings_phone_silent_mode.xml new file mode 100644 index 000000000..1866d1036 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_phone_silent_mode.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/xml/devicesettings_pixoo.xml b/app/src/main/res/xml/devicesettings_pixoo.xml new file mode 100644 index 000000000..6120c78cb --- /dev/null +++ b/app/src/main/res/xml/devicesettings_pixoo.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/xml/devicesettings_prefix_notification_with_app.xml b/app/src/main/res/xml/devicesettings_prefix_notification_with_app.xml new file mode 100644 index 000000000..4398bb873 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_prefix_notification_with_app.xml @@ -0,0 +1,9 @@ + + + + diff --git a/app/src/main/res/xml/devicesettings_preview_message_in_title.xml b/app/src/main/res/xml/devicesettings_preview_message_in_title.xml new file mode 100644 index 000000000..cfd18189b --- /dev/null +++ b/app/src/main/res/xml/devicesettings_preview_message_in_title.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/devicesettings_send_app_notifications.xml b/app/src/main/res/xml/devicesettings_send_app_notifications.xml new file mode 100644 index 000000000..e6b5dab84 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_send_app_notifications.xml @@ -0,0 +1,9 @@ + + + + diff --git a/app/src/main/res/xml/devicesettings_sleep_mode_schedule.xml b/app/src/main/res/xml/devicesettings_sleep_mode_schedule.xml new file mode 100644 index 000000000..8dce9b59c --- /dev/null +++ b/app/src/main/res/xml/devicesettings_sleep_mode_schedule.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + diff --git a/app/src/main/res/xml/devicesettings_sony_headphones_device_info.xml b/app/src/main/res/xml/devicesettings_sony_headphones_device_info.xml index 0997efcfb..6bfc4c275 100644 --- a/app/src/main/res/xml/devicesettings_sony_headphones_device_info.xml +++ b/app/src/main/res/xml/devicesettings_sony_headphones_device_info.xml @@ -14,5 +14,14 @@ android:title="@string/audio_codec" app:useSimpleSummaryProvider="true" /> + + diff --git a/app/src/main/res/xml/devicesettings_sony_headphones_protocol_version.xml b/app/src/main/res/xml/devicesettings_sony_headphones_protocol_version.xml new file mode 100644 index 000000000..c9e1d7337 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_sony_headphones_protocol_version.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/app/src/main/res/xml/devicesettings_spo_automatic_enable.xml b/app/src/main/res/xml/devicesettings_spo_automatic_enable.xml new file mode 100644 index 000000000..f59cd0705 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_spo_automatic_enable.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/app/src/main/res/xml/devicesettings_trusleep.xml b/app/src/main/res/xml/devicesettings_trusleep.xml new file mode 100644 index 000000000..7a65510a3 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_trusleep.xml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/app/src/main/res/xml/devicesettings_vibrationpatterns.xml b/app/src/main/res/xml/devicesettings_vibrationpatterns.xml index 6e244f6a5..ec0acaa6d 100644 --- a/app/src/main/res/xml/devicesettings_vibrationpatterns.xml +++ b/app/src/main/res/xml/devicesettings_vibrationpatterns.xml @@ -1,5 +1,6 @@ - + + android:key="vibration_profile_key_app_alerts" + android:persistent="false" + android:title="@string/pref_screen_notification_profile_generic"> @@ -23,27 +24,28 @@ android:entries="@array/vibration_profile" android:entryValues="@array/vibration_profile_values" android:key="huami_vibration_profile_app_alerts" - android:title="@string/miband_prefs_vibration" - android:summary="%s" /> + android:summary="%s" + android:title="@string/miband_prefs_vibration" /> + android:title="@string/pref_title_notifications_repetitions" + app:useSimpleSummaryProvider="true" /> + android:persistent="false" + android:title="@string/vibration_try" /> + android:key="vibration_profile_key_incoming_call" + android:persistent="false" + android:title="@string/pref_screen_notification_profile_incoming_call"> @@ -53,27 +55,28 @@ android:entries="@array/vibration_profile" android:entryValues="@array/vibration_profile_values" android:key="huami_vibration_profile_incoming_call" - android:title="@string/miband_prefs_vibration" - android:summary="%s" /> + android:summary="%s" + android:title="@string/miband_prefs_vibration" /> + android:title="@string/pref_title_notifications_repetitions" + app:useSimpleSummaryProvider="true" /> + android:persistent="false" + android:title="@string/vibration_try" /> + android:key="vibration_profile_key_incoming_sms" + android:persistent="false" + android:title="@string/pref_screen_notification_profile_sms"> @@ -83,27 +86,28 @@ android:entries="@array/vibration_profile" android:entryValues="@array/vibration_profile_values" android:key="huami_vibration_profile_incoming_sms" - android:title="@string/miband_prefs_vibration" - android:summary="%s" /> + android:summary="%s" + android:title="@string/miband_prefs_vibration" /> + android:title="@string/pref_title_notifications_repetitions" + app:useSimpleSummaryProvider="true" /> + android:persistent="false" + android:title="@string/vibration_try" /> + android:key="vibration_profile_key_goal_notification" + android:persistent="false" + android:title="@string/mi2_prefs_goal_notification"> @@ -113,27 +117,28 @@ android:entries="@array/vibration_profile" android:entryValues="@array/vibration_profile_values" android:key="huami_vibration_profile_goal_notification" - android:title="@string/miband_prefs_vibration" - android:summary="%s" /> + android:summary="%s" + android:title="@string/miband_prefs_vibration" /> + android:title="@string/pref_title_notifications_repetitions" + app:useSimpleSummaryProvider="true" /> + android:persistent="false" + android:title="@string/vibration_try" /> + android:key="vibration_profile_key_alarm" + android:persistent="false" + android:title="@string/vibration_profile_alarm_clock"> @@ -143,27 +148,28 @@ android:entries="@array/vibration_profile" android:entryValues="@array/vibration_profile_values" android:key="huami_vibration_profile_alarm" - android:title="@string/miband_prefs_vibration" - android:summary="%s" /> + android:summary="%s" + android:title="@string/miband_prefs_vibration" /> + android:title="@string/pref_title_notifications_repetitions" + app:useSimpleSummaryProvider="true" /> + android:persistent="false" + android:title="@string/vibration_try" /> + android:key="vibration_profile_key_idle_alerts" + android:persistent="false" + android:title="@string/pref_screen_notification_idle_alerts"> @@ -173,27 +179,28 @@ android:entries="@array/vibration_profile" android:entryValues="@array/vibration_profile_values" android:key="huami_vibration_profile_idle_alerts" - android:title="@string/miband_prefs_vibration" - android:summary="%s" /> + android:summary="%s" + android:title="@string/miband_prefs_vibration" /> + android:title="@string/pref_title_notifications_repetitions" + app:useSimpleSummaryProvider="true" /> + android:persistent="false" + android:title="@string/vibration_try" /> + android:key="vibration_profile_key_event_reminder" + android:persistent="false" + android:title="@string/pref_screen_notification_profile_event_reminder"> @@ -203,27 +210,28 @@ android:entries="@array/vibration_profile" android:entryValues="@array/vibration_profile_values" android:key="huami_vibration_profile_event_reminder" - android:title="@string/miband_prefs_vibration" - android:summary="%s" /> + android:summary="%s" + android:title="@string/miband_prefs_vibration" /> + android:title="@string/pref_title_notifications_repetitions" + app:useSimpleSummaryProvider="true" /> + android:persistent="false" + android:title="@string/vibration_try" /> + android:key="vibration_profile_key_find_band" + android:persistent="false" + android:title="@string/pref_screen_notification_profile_find_device"> @@ -233,27 +241,28 @@ android:entries="@array/vibration_profile" android:entryValues="@array/vibration_profile_values" android:key="huami_vibration_profile_find_band" - android:title="@string/miband_prefs_vibration" - android:summary="%s" /> + android:summary="%s" + android:title="@string/miband_prefs_vibration" /> + android:title="@string/pref_title_notifications_repetitions" + app:useSimpleSummaryProvider="true" /> + android:persistent="false" + android:title="@string/vibration_try" /> + android:key="vibration_profile_key_todo_list" + android:persistent="false" + android:title="@string/pref_screen_notification_profile_todo_list"> @@ -263,27 +272,28 @@ android:entries="@array/vibration_profile" android:entryValues="@array/vibration_profile_values" android:key="huami_vibration_profile_todo_list" - android:title="@string/miband_prefs_vibration" - android:summary="%s" /> + android:summary="%s" + android:title="@string/miband_prefs_vibration" /> + android:title="@string/pref_title_notifications_repetitions" + app:useSimpleSummaryProvider="true" /> + android:persistent="false" + android:title="@string/vibration_try" /> + android:key="vibration_profile_key_schedule" + android:persistent="false" + android:title="@string/pref_screen_notification_profile_schedule"> @@ -293,20 +303,21 @@ android:entries="@array/vibration_profile" android:entryValues="@array/vibration_profile_values" android:key="huami_vibration_profile_schedule" - android:title="@string/miband_prefs_vibration" - android:summary="%s" /> + android:summary="%s" + android:title="@string/miband_prefs_vibration" /> + android:title="@string/pref_title_notifications_repetitions" + app:useSimpleSummaryProvider="true" /> + android:persistent="false" + android:title="@string/vibration_try" /> diff --git a/app/src/main/res/xml/devicesettings_vitality_score.xml b/app/src/main/res/xml/devicesettings_vitality_score.xml new file mode 100644 index 000000000..eb109d7fc --- /dev/null +++ b/app/src/main/res/xml/devicesettings_vitality_score.xml @@ -0,0 +1,23 @@ + + + + + + + + + diff --git a/app/src/main/res/xml/devicesettings_wearmode.xml b/app/src/main/res/xml/devicesettings_wearmode.xml new file mode 100644 index 000000000..7584dd982 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_wearmode.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/xml/devicesettings_widgets.xml b/app/src/main/res/xml/devicesettings_widgets.xml new file mode 100644 index 000000000..5c137f993 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_widgets.xml @@ -0,0 +1,7 @@ + + + + diff --git a/app/src/main/res/xml/devicesettings_workmode.xml b/app/src/main/res/xml/devicesettings_workmode.xml new file mode 100644 index 000000000..51edc5dbf --- /dev/null +++ b/app/src/main/res/xml/devicesettings_workmode.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/xml/devicesettings_workout_activity_types.xml b/app/src/main/res/xml/devicesettings_workout_activity_types.xml new file mode 100644 index 000000000..5213d34e2 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_workout_activity_types.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/xml/devicesettings_xiaomi_displayitems.xml b/app/src/main/res/xml/devicesettings_xiaomi_displayitems.xml new file mode 100644 index 000000000..1ca0247c7 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_xiaomi_displayitems.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index c6f9a2b05..e8451b3d4 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -18,6 +18,13 @@ android:layout="@layout/preference_checkbox" android:title="@string/pref_title_general_autoconnectonbluetooth" app:iconSpaceReserved="false" /> + - - + + + + + + + + + . */ +package nodomain.freeyourgadget.gadgetbridge.devices.huawei; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.Arrays; + +public class TestHuaweiCrypto { + + private void printByteArrayAsHex(String name, byte[] array) { + System.out.print(name + ": ["); + for (int i = 0; i < array.length - 1; i++) { + System.out.print(Integer.toHexString(array[i] & 0xFF) + ", "); + } + System.out.println(Integer.toHexString(array[array.length - 1] & 0xFF) + "]"); + } + + private void printIntArray(String name, int[] array) { + System.out.print(name + ": ["); + for (int i = 0; i < array.length - 1; i++) { + System.out.print(array[i] + ", "); + } + System.out.println(array[array.length - 1] + "]"); + } + + @Test + public void testGenerateNonce() { + // The function output contains randomness, so we test multiple times + + // We also check how often each byte is present, and that the difference isn't too high + // Note that this may fail due to the randomness + int[] bytePresent = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + Assert.assertEquals(256, bytePresent.length); + + for (int i = 0; i < 1000; i++) { + byte[] output = HuaweiCrypto.generateNonce(); + + printByteArrayAsHex("Output", output); + + Assert.assertEquals(16, output.length); + + for (byte b : output) { + bytePresent[b & 0xFF] += 1; + } + } + + printIntArray("Bytes present", bytePresent); + + int minCount = Integer.MAX_VALUE; + int maxCount = Integer.MIN_VALUE; + for (int c : bytePresent) { + minCount = Math.min(c, minCount); + maxCount = Math.max(c, maxCount); + } + + // The limit here is quite arbitrary, erring on the high side + if (maxCount - minCount > 60) { + Assert.fail("Difference in byte counts is suspiciously high, check the randomness of the nonce."); + } + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/TestHuaweiPacket.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/TestHuaweiPacket.java new file mode 100644 index 000000000..a6b761242 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/TestHuaweiPacket.java @@ -0,0 +1,269 @@ +/* Copyright (C) 2023 Martin.JM + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ + +package nodomain.freeyourgadget.gadgetbridge.devices.huawei; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FindPhone; + +public class TestHuaweiPacket { + + HuaweiPacket.ParamsProvider paramsProvider = new HuaweiPacket.ParamsProvider() { + + @Override + public byte getDeviceSupportType() { + return 0; + } + + @Override + public byte[] getSecretKey() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public byte[] getIv() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public boolean areTransactionsCrypted() { + return false; + } + + @Override + public int getMtu() { + return 0; + } + + @Override + public int getSliceSize() { + return 0xF4; + } + }; + + HuaweiPacket.ParamsProvider paramsProviderEncrypt = new HuaweiPacket.ParamsProvider() { + + @Override + public byte getDeviceSupportType() { + return 0; + } + + @Override + public byte[] getSecretKey() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public byte[] getIv() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public boolean areTransactionsCrypted() { + return true; + } + + @Override + public int getMtu() { + return 0; + } + + @Override + public int getSliceSize() { + return 0xF4; + } + }; + + HuaweiPacket.ParamsProvider paramsProviderSmallSlice = new HuaweiPacket.ParamsProvider() { + + @Override + public byte getDeviceSupportType() { + return 0; + } + + @Override + public byte[] getSecretKey() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public byte[] getIv() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public boolean areTransactionsCrypted() { + return false; + } + + @Override + public int getMtu() { + return 0; + } + + @Override + public int getSliceSize() { + return 0x10; + } + }; + + @Test + public void testEmptyPacketParse() { + byte[] input = {}; + + HuaweiPacket packet = new HuaweiPacket(paramsProvider); + + try { + packet.parse(input); + } catch (HuaweiPacket.LengthMismatchException e) { + // Pass + } catch (HuaweiPacket.ParseException e) { + Assert.fail(); + } + } + + @Test + public void testUnknownUnencryptedPacketParse() throws HuaweiPacket.ParseException { + byte[] input = {0x5a, 0x00, 0x07, 0x00, 0x7f, 0x7f, 0x01, 0x02, 0x03, 0x04, 0x40, (byte) 0xb6}; + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x01, (short) 0x0304); + + HuaweiPacket packet = new HuaweiPacket(paramsProvider); + packet = packet.parse(input); + + Assert.assertEquals(HuaweiPacket.class, packet.getClass()); + Assert.assertEquals(0x7f, packet.serviceId); + Assert.assertEquals(0x7f, packet.commandId); + Assert.assertFalse(packet.isEncrypted); + Assert.assertTrue(packet.complete); + Assert.assertEquals(expectedTlv, packet.getTlv()); + } + + @Test + public void testUnknownEncryptedPacketParse() throws HuaweiPacket.ParseException { + byte[] input = {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x7f, (byte) 0x7f, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0x9e, (byte) 0x40, (byte) 0xe1, (byte) 0xea, (byte) 0x15, (byte) 0xf6, (byte) 0x50, (byte) 0x80, (byte) 0x8c, (byte) 0x45, (byte) 0x19, (byte) 0xd5, (byte) 0x2a, (byte) 0xbb, (byte) 0x29, (byte) 0xb8, (byte) 0xD5, (byte) 0x24}; + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x01, (short) 0x0304); + + HuaweiPacket packet = new HuaweiPacket(paramsProvider); + packet = packet.parse(input); + + Assert.assertEquals(HuaweiPacket.class, packet.getClass()); + Assert.assertEquals(0x7f, packet.serviceId); + Assert.assertEquals(0x7f, packet.commandId); + Assert.assertTrue(packet.isEncrypted); + Assert.assertTrue(packet.complete); + Assert.assertEquals(expectedTlv, packet.getTlv()); + } + + @Test + public void testKnownEncryptedPacketParse() throws HuaweiPacket.ParseException { + byte[] input = {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x0b, (byte) 0x01, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0x28, (byte) 0x00, (byte) 0x99, (byte) 0x6f, (byte) 0x2a, (byte) 0xcb, (byte) 0x62, (byte) 0x3a, (byte) 0xe6, (byte) 0x54, (byte) 0x28, (byte) 0x54, (byte) 0xf8, (byte) 0xab, (byte) 0x54, (byte) 0x83, (byte) 0xf4, (byte) 0xf4}; + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x01, false); + + HuaweiPacket packet = new HuaweiPacket(paramsProvider); + packet = packet.parse(input); + + Assert.assertEquals(FindPhone.Response.class, packet.getClass()); + Assert.assertEquals(0x0b, packet.serviceId); + Assert.assertEquals(0x01, packet.commandId); + Assert.assertTrue(packet.isEncrypted); + Assert.assertTrue(packet.complete); + Assert.assertEquals(expectedTlv, packet.getTlv()); + } + + @Test + public void testUnencryptedUnslicedSerialize() throws HuaweiPacket.CryptoException { + byte serviceId = 0x01; + byte commandId = 0x02; + HuaweiTLV tlv = new HuaweiTLV() + .put(0x03, 0x05060708); + + byte[] expected = {(byte) 0x5a, (byte) 0x00, (byte) 0x09, (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04, (byte) 0x05, (byte) 0x06, (byte) 0x07, (byte) 0x08, (byte) 0xA4, (byte) 0xF0}; + + HuaweiPacket packet = new HuaweiPacket(paramsProvider); + packet.isSliced = false; + packet.isEncrypted = false; + packet.serviceId = serviceId; + packet.commandId = commandId; + packet.setTlv(tlv); + + List output = packet.serialize(); + + Assert.assertEquals(1, output.size()); + Assert.assertArrayEquals(expected, output.get(0)); + } + + @Test + public void testEncryptedUnslicedSerialize() throws HuaweiPacket.CryptoException { + byte serviceId = 0x01; + byte commandId = 0x02; + HuaweiTLV tlv = new HuaweiTLV() + .put(0x03, 0x05060708); + + byte[] expected = {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0x3b, (byte) 0x89, (byte) 0xfc, (byte) 0x79, (byte) 0xd9, (byte) 0x05, (byte) 0x5e, (byte) 0xed, (byte) 0x52, (byte) 0x35, (byte) 0xfe, (byte) 0x16, (byte) 0xa0, (byte) 0x8a, (byte) 0x4d, (byte) 0x53, (byte) 0x93, (byte) 0xc7}; + + HuaweiPacket packet = new HuaweiPacket(paramsProviderEncrypt); + packet.isSliced = false; + packet.isEncrypted = true; + packet.serviceId = serviceId; + packet.commandId = commandId; + packet.setTlv(tlv); + + List output = packet.serialize(); + + Assert.assertEquals(1, output.size()); + Assert.assertArrayEquals(expected, output.get(0)); + } + + @Test + public void testUnencryptedSlicedSerialize() throws HuaweiPacket.CryptoException { + byte serviceId = 0x01; + byte commandId = 0x02; + HuaweiTLV tlv = new HuaweiTLV() + .put(0x01, 0x00) + .put(0x02, 0x00) + .put(0x03, 0x00) + .put(0x04, 0x00); + + byte[] expected1 = {(byte) 0x5a, (byte) 0x00, (byte) 0x0b, (byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x01, (byte) 0x04, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x02, (byte) 0xcc, (byte) 0x98}; + byte[] expected2 = {(byte) 0x5a, (byte) 0x00, (byte) 0x0b, (byte) 0x02, (byte) 0x01, (byte) 0x04, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x03, (byte) 0x04, (byte) 0x00, (byte) 0x00, (byte) 0xfa, (byte) 0xd3}; + byte[] expected3 = {(byte) 0x5a, (byte) 0x00, (byte) 0x0a, (byte) 0x03, (byte) 0x02, (byte) 0x00, (byte) 0x00, (byte) 0x04, (byte) 0x04, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x37, (byte) 0xca}; + + HuaweiPacket packet = new HuaweiPacket(paramsProviderSmallSlice); + packet.isSliced = true; + packet.isEncrypted = false; + packet.serviceId = serviceId; + packet.commandId = commandId; + packet.setTlv(tlv); + + List output = packet.serialize(); + + Assert.assertEquals(3, output.size()); + Assert.assertArrayEquals(expected1, output.get(0)); + Assert.assertArrayEquals(expected2, output.get(1)); + Assert.assertArrayEquals(expected3, output.get(2)); + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/TestHuaweiTLV.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/TestHuaweiTLV.java new file mode 100644 index 000000000..02fcdb665 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/TestHuaweiTLV.java @@ -0,0 +1,532 @@ +/* Copyright (C) 2022 Martin.JM + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.huawei; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; + +public class TestHuaweiTLV { + + HuaweiPacket.ParamsProvider secretsProvider = new HuaweiPacket.ParamsProvider() { + @Override + public byte getDeviceSupportType() { + return 0; + } + + @Override + public byte[] getSecretKey() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public byte[] getIv() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public boolean areTransactionsCrypted() { + return false; + } + + @Override + public int getMtu() { + return 0; + } + + @Override + public int getSliceSize() { + return 0xF4; + } + }; + + @Test + public void testSerializeEmpty() { + byte[] expectedOutput = {}; + + HuaweiTLV huaweiTLV = new HuaweiTLV(); + + Assert.assertArrayEquals(expectedOutput, huaweiTLV.serialize()); + } + + @Test + public void testSerialize() { + ArrayList input = new ArrayList<>(); + input.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {})); + input.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {0x42})); + byte[] expectedOutput = {0x01, 0x00, 0x01, 0x01, 0x42}; + + HuaweiTLV huaweiTLV = new HuaweiTLV(); + huaweiTLV.valueMap = input; + + Assert.assertArrayEquals(expectedOutput, huaweiTLV.serialize()); + } + + @Test + public void testPutEmptyTag() { + int tag = 0x01; + ArrayList expectedValueMap = new ArrayList<>(); + expectedValueMap.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {})); + + HuaweiTLV huaweiTLV = new HuaweiTLV().put(tag); + + Assert.assertEquals(expectedValueMap, huaweiTLV.valueMap); + } + + @Test + public void testPutNullByteArray() { + int tag = 0x01; + byte[] input = null; + ArrayList expectedValueMap = new ArrayList<>(); + + //noinspection ConstantConditions + HuaweiTLV huaweiTLV = new HuaweiTLV().put(tag, input); + + Assert.assertEquals(expectedValueMap, huaweiTLV.valueMap); + } + + @Test + public void testPutByteArray() { + int tag = 0x01; + byte[] input = {0x01, 0x02, 0x03}; + ArrayList expectedValueMap = new ArrayList<>(); + expectedValueMap.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {0x01, 0x02, 0x03})); + + HuaweiTLV huaweiTLV = new HuaweiTLV() + .put(tag, input); + + Assert.assertEquals(expectedValueMap, huaweiTLV.valueMap); + } + + @Test + public void testPutByte() { + int tag = 0x01; + byte input = 0x13; + ArrayList expectedValueMap = new ArrayList<>(); + expectedValueMap.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {0x13})); + + HuaweiTLV huaweiTLV = new HuaweiTLV() + .put(tag, input); + + Assert.assertEquals(expectedValueMap, huaweiTLV.valueMap); + } + + @Test + public void testPutBooleans() { + int tag1 = 0x01; + int tag2 = 0x02; + ArrayList expectedValueMap = new ArrayList<>(); + expectedValueMap.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {0x01})); + expectedValueMap.add(new HuaweiTLV.TLV((byte) 0x02, new byte[] {0x00})); + + HuaweiTLV huaweiTLV = new HuaweiTLV() + .put(tag1, true) + .put(tag2, false); + + Assert.assertEquals(expectedValueMap, huaweiTLV.valueMap); + } + + @Test + public void testPutInt() { + int tag = 0x01; + int input = 0xDEADBEEF; + ArrayList expectedValueMap = new ArrayList<>(); + expectedValueMap.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {(byte) 0xDE, (byte) 0xAD, (byte) 0xBE, (byte) 0xEF})); + + HuaweiTLV huaweiTLV = new HuaweiTLV() + .put(tag, input); + + Assert.assertEquals(expectedValueMap, huaweiTLV.valueMap); + } + + @Test + public void testPutShort() { + int tag = 0x01; + short input = (short) 0xCAFE; + ArrayList expectedValueMap = new ArrayList<>(); + expectedValueMap.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {(byte) 0xCA, (byte) 0xFE})); + + HuaweiTLV huaweiTLV = new HuaweiTLV() + .put(tag, input); + + Assert.assertEquals(expectedValueMap, huaweiTLV.valueMap); + } + + @Test + public void testPutString() { + int tag = 0x01; + String input = "Hello world!"; + ArrayList expectedValueMap = new ArrayList<>(); + expectedValueMap.add(new HuaweiTLV.TLV( + (byte) 0x01, + new byte[] {0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x77, 0x6F, 0x72, 0x6C, 0x64, 0x21} + )); + + HuaweiTLV huaweiTLV = new HuaweiTLV() + .put(tag, input); + + Assert.assertEquals(expectedValueMap, huaweiTLV.valueMap); + } + + @Test + public void testPutHuaweiTLV() { + int tag = 0x01; + HuaweiTLV input = new HuaweiTLV().put(0x01, (short) 0x1337); + ArrayList expectedValueMap = new ArrayList<>(); + expectedValueMap.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {0x01, 0x02, 0x13, 0x37})); + + HuaweiTLV huaweiTLV = new HuaweiTLV() + .put(tag, input); + + Assert.assertEquals(expectedValueMap, huaweiTLV.valueMap); + } + + @Test + public void testPutMultipleEqualEmptyTags() { + int tag = 0x01; + ArrayList expectedValueMap = new ArrayList<>(); + expectedValueMap.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {})); + expectedValueMap.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {})); + + HuaweiTLV huaweiTLV = new HuaweiTLV() + .put(tag) + .put(tag); + + Assert.assertEquals(expectedValueMap, huaweiTLV.valueMap); + } + + @Test + public void testParseEmpty() { + byte[] input = {}; + ArrayList expectedValueMap = new ArrayList<>(); + + HuaweiTLV huaweiTLV = new HuaweiTLV() + .parse(input); + + Assert.assertEquals(expectedValueMap, huaweiTLV.valueMap); + } + + @Test + public void testParseSingleByte() { + byte[] input = {0x01, 0x01, 0x01}; + ArrayList expectedValueMap = new ArrayList<>(); + expectedValueMap.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {0x01})); + + HuaweiTLV huaweiTLV = new HuaweiTLV() + .parse(input); + + Assert.assertEquals(expectedValueMap, huaweiTLV.valueMap); + } + + @Test + public void testParseBytes() { + byte[] input = {0x01, 0x04, (byte) 0xDE, (byte) 0xAD, (byte) 0xBE, (byte) 0xEF}; + ArrayList expectedValueMap = new ArrayList<>(); + expectedValueMap.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {(byte) 0xDE, (byte) 0xAD, (byte) 0xBE, (byte) 0xEF})); + + HuaweiTLV huaweiTLV = new HuaweiTLV() + .parse(input); + + Assert.assertEquals(expectedValueMap, huaweiTLV.valueMap); + } + + @Test + public void testParseZeroOffsetLength() { + byte[] input = {}; + ArrayList expectedValueMap = new ArrayList<>(); + + HuaweiTLV huaweiTLV = new HuaweiTLV() + .parse(input, 0, 0); + + Assert.assertEquals(expectedValueMap, huaweiTLV.valueMap); + } + + @Test(expected = ArrayIndexOutOfBoundsException.class) + public void testParseMalformed() { + byte[] input = {(byte) 0x01, (byte) 0x01}; + new HuaweiTLV() + .parse(input); + Assert.fail(); + } + + @Test + public void testParseOffsetLength() { + byte[] input = {(byte) 0x90, (byte) 0x90, (byte) 0x90, 0x01, 0x00}; + int offset = 3; + int length = 2; + ArrayList expectedValueMap = new ArrayList<>(); + expectedValueMap.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {})); + + HuaweiTLV huaweiTLV = new HuaweiTLV() + .parse(input, offset, length); + + Assert.assertEquals(expectedValueMap, huaweiTLV.valueMap); + } + + @Test(expected = ArrayIndexOutOfBoundsException.class) + public void testParseWrongOffsetLength() { + byte[] input = {}; + new HuaweiTLV() + .parse(input, 1, 1); + Assert.fail(); + } + + @Test + public void testGetBytesEmpty() throws HuaweiPacket.MissingTagException { + int tag = 0x01; + ArrayList input = new ArrayList<>(); + input.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {})); + byte[] expectedOutput = new byte[] {}; + + HuaweiTLV huaweiTLV = new HuaweiTLV(); + huaweiTLV.valueMap = input; + + Assert.assertArrayEquals(expectedOutput, huaweiTLV.getBytes(tag)); + } + + @Test + public void testGetBytes() throws HuaweiPacket.MissingTagException { + int tag = 0x01; + ArrayList input = new ArrayList<>(); + input.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {0x01, 0x02, 0x03})); + byte[] expectedOutput = new byte[] {0x01, 0x02, 0x03}; + + HuaweiTLV huaweiTLV = new HuaweiTLV(); + huaweiTLV.valueMap = input; + + Assert.assertArrayEquals(expectedOutput, huaweiTLV.getBytes(tag)); + } + + @Test + public void testGetByte() throws HuaweiPacket.MissingTagException { + int tag = 0x01; + ArrayList input = new ArrayList<>(); + input.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {0x04})); + Byte expectedOutput = 0x04; + + HuaweiTLV huaweiTLV = new HuaweiTLV(); + huaweiTLV.valueMap = input; + + Assert.assertEquals(expectedOutput, huaweiTLV.getByte(tag)); + } + + @Test + public void testGetBooleans() throws HuaweiPacket.MissingTagException { + ArrayList input = new ArrayList<>(); + input.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {0x01})); + input.add(new HuaweiTLV.TLV((byte) 0x02, new byte[] {0x00})); + + HuaweiTLV huaweiTLV = new HuaweiTLV(); + huaweiTLV.valueMap = input; + + Assert.assertEquals(true, huaweiTLV.getBoolean(0x01)); + Assert.assertEquals(false, huaweiTLV.getBoolean(0x02)); + } + + @Test + public void testGetInteger() throws HuaweiPacket.MissingTagException { + int tag = 0x01; + ArrayList input = new ArrayList<>(); + input.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {(byte) 0xDE, (byte) 0xAD, (byte) 0xBE, (byte) 0xEF})); + Integer expectedOutput = 0xDEADBEEF; + + HuaweiTLV huaweiTLV = new HuaweiTLV(); + huaweiTLV.valueMap = input; + + Assert.assertEquals(expectedOutput, huaweiTLV.getInteger(tag)); + } + + @Test + public void testGetShort() throws HuaweiPacket.MissingTagException { + int tag = 0x01; + ArrayList input = new ArrayList<>(); + input.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {(byte) 0xCA, (byte) 0xFE})); + Short expectedOutput = (short) 0xCAFE; + + HuaweiTLV huaweiTLV = new HuaweiTLV(); + huaweiTLV.valueMap = input; + + Assert.assertEquals(expectedOutput, huaweiTLV.getShort(tag)); + } + + @Test + public void testGetString() throws HuaweiPacket.MissingTagException { + int tag = 0x01; + ArrayList input = new ArrayList<>(); + input.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x77, 0x6F, 0x72, 0x6C, 0x64, 0x21})); + String expectedOutput = "Hello world!"; + + HuaweiTLV huaweiTLV = new HuaweiTLV(); + huaweiTLV.valueMap = input; + + Assert.assertEquals(expectedOutput, huaweiTLV.getString(tag)); + } + + @Test + public void testGetObject() throws HuaweiPacket.MissingTagException { + int tag = 0x01; + ArrayList input = new ArrayList<>(); + input.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {0x01, 0x00})); + HuaweiTLV expectedOutput = new HuaweiTLV().put(0x01); + + HuaweiTLV huaweiTLV = new HuaweiTLV(); + huaweiTLV.valueMap = input; + + // assertEquals currently tests if the objects are the same, thus this would fail + // Assert.assertEquals(expectedOutput, huaweiTLV.getObject(tag)); + + HuaweiTLV result = huaweiTLV.getObject(tag); + + Assert.assertEquals(expectedOutput.valueMap, result.valueMap); + } + + @Test + public void testContains() { + int existingTag = 0x01; + int nonExistingTag = 0x02; + ArrayList input = new ArrayList<>(); + input.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {})); + + HuaweiTLV huaweiTLV = new HuaweiTLV(); + huaweiTLV.valueMap = input; + + Assert.assertTrue(huaweiTLV.contains(existingTag)); + Assert.assertFalse(huaweiTLV.contains(nonExistingTag)); + } + + @Test + public void testRemoveExisting() { + int tag = 0x01; + ArrayList input = new ArrayList<>(); + input.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {0x13, 0x37})); + input.add(new HuaweiTLV.TLV((byte) 0x02, new byte[] {})); + byte[] expectedOutput = {0x13, 0x37}; + ArrayList expectedValueMap = new ArrayList<>(); + expectedValueMap.add(new HuaweiTLV.TLV((byte) 0x02, new byte[] {})); + + HuaweiTLV huaweiTLV = new HuaweiTLV(); + huaweiTLV.valueMap = input; + + Assert.assertArrayEquals(expectedOutput, huaweiTLV.remove(tag)); + Assert.assertEquals(expectedValueMap, huaweiTLV.valueMap); + } + + @Test + public void testRemoveNonExisting() { + int tag = 0x02; + ArrayList input = new ArrayList<>(); + input.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {0x13, 0x37})); + + HuaweiTLV huaweiTLV = new HuaweiTLV(); + huaweiTLV.valueMap = input; + + Assert.assertNull(huaweiTLV.remove(tag)); + Assert.assertEquals(input, huaweiTLV.valueMap); + } + + @Test + public void testRemoveDouble() { + int tag = 0x01; + ArrayList input = new ArrayList<>(); + input.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {(byte) 0xCA})); + input.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {(byte) 0xFE})); + byte[] expectedOutput1 = {(byte) 0xFE}; + byte[] expectedOutput2 = {(byte) 0xCA}; + ArrayList expectedValueMap1 = new ArrayList<>(); + expectedValueMap1.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {(byte) 0xCA})); + ArrayList expectedValueMap2 = new ArrayList<>(); + + HuaweiTLV huaweiTLV = new HuaweiTLV(); + huaweiTLV.valueMap = input; + + Assert.assertArrayEquals(expectedOutput1, huaweiTLV.remove(tag)); + Assert.assertEquals(expectedValueMap1, huaweiTLV.valueMap); + Assert.assertArrayEquals(expectedOutput2, huaweiTLV.remove(tag)); + Assert.assertEquals(expectedValueMap2, huaweiTLV.valueMap); + } + + @Test + public void testToStringEmpty() { + ArrayList input = new ArrayList<>(); + String expectedOutput = "Empty"; + + HuaweiTLV huaweiTLV = new HuaweiTLV(); + huaweiTLV.valueMap = input; + + Assert.assertEquals(expectedOutput, huaweiTLV.toString()); + } + + @Test + public void testToString() { + ArrayList input = new ArrayList<>(); + input.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {0x01, 0x02})); + input.add(new HuaweiTLV.TLV((byte) 0x02, new byte[] {0x03, 0x04})); + String expectedOutput = "{tag: 1 - Value: 0102} - {tag: 2 - Value: 0304}"; + + HuaweiTLV huaweiTLV = new HuaweiTLV(); + huaweiTLV.valueMap = input; + + Assert.assertEquals(expectedOutput, huaweiTLV.toString()); + } + + /** + * Following test also depends on the HuaweiCrypto class functioning correctly + */ + @Test + public void testEncrypt() throws HuaweiCrypto.CryptoException { + ArrayList input = new ArrayList<>(); + input.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {(byte) 0xCA, (byte) 0xFE})); + + byte[] expectedCiphertext = {(byte) 0x0E, (byte) 0xA0, (byte) 0x01, (byte) 0xBB, (byte) 0x1E, (byte) 0xDA, (byte) 0xCB, (byte) 0x09, (byte) 0x83, (byte) 0x20, (byte) 0x40, (byte) 0x7D, (byte) 0x97, (byte) 0x1B, (byte) 0xF6, (byte) 0xD0}; + ArrayList expectedValueMap = new ArrayList<>(); + expectedValueMap.add(new HuaweiTLV.TLV((byte) 0x7C, new byte[] {0x01})); + expectedValueMap.add(new HuaweiTLV.TLV((byte) 0x7D, secretsProvider.getIv())); + expectedValueMap.add(new HuaweiTLV.TLV((byte) 0x7E, expectedCiphertext)); + + HuaweiTLV huaweiTLV = new HuaweiTLV(); + huaweiTLV.valueMap = input; + + HuaweiTLV encryptedTlv = huaweiTLV.encrypt(secretsProvider); + + Assert.assertEquals(input, huaweiTLV.valueMap); + Assert.assertEquals(expectedValueMap, encryptedTlv.valueMap); + } + + /** + * Following test also depends on the HuaweiCrypto class functioning correctly + */ + @Test + public void testDecrypt() throws HuaweiCrypto.CryptoException, HuaweiPacket.MissingTagException { + byte[] ciphertext = {(byte) 0x0E, (byte) 0xA0, (byte) 0x01, (byte) 0xBB, (byte) 0x1E, (byte) 0xDA, (byte) 0xCB, (byte) 0x09, (byte) 0x83, (byte) 0x20, (byte) 0x40, (byte) 0x7D, (byte) 0x97, (byte) 0x1B, (byte) 0xF6, (byte) 0xD0}; + ArrayList input = new ArrayList<>(); + input.add(new HuaweiTLV.TLV((byte) 0x7C, new byte[] {0x01})); + input.add(new HuaweiTLV.TLV((byte) 0x7D, secretsProvider.getIv())); + input.add(new HuaweiTLV.TLV((byte) 0x7E, ciphertext)); + + ArrayList expectedValueMap = new ArrayList<>(); + expectedValueMap.add(new HuaweiTLV.TLV((byte) 0x01, new byte[] {(byte) 0xCA, (byte) 0xFE})); + + HuaweiTLV huaweiTLV = new HuaweiTLV(); + huaweiTLV.valueMap = input; + + huaweiTLV.decrypt(secretsProvider); + + Assert.assertEquals(expectedValueMap, huaweiTLV.valueMap); + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/TestVarInt.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/TestVarInt.java new file mode 100644 index 000000000..fde67f548 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/TestVarInt.java @@ -0,0 +1,72 @@ +/* Copyright (C) 2022 Martin.JM + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.huawei; + +import org.junit.Assert; +import org.junit.Test; + +public class TestVarInt { + + private void testValue(int intValue, byte[] bytesValue, int size) { + Assert.assertEquals(size, VarInt.getVarIntSize(intValue)); + Assert.assertEquals(intValue, VarInt.getVarIntValue(bytesValue, 0)); + Assert.assertArrayEquals(bytesValue, VarInt.putVarIntValue(intValue)); + + VarInt varInt = new VarInt(bytesValue, 0); + Assert.assertEquals(size, varInt.size); + Assert.assertEquals(intValue, varInt.dValue); + Assert.assertArrayEquals(bytesValue, varInt.eValue); + } + + @Test + public void testZero() { + testValue(0, new byte[]{0}, 1); + } + + @Test + public void testSingleValue() { + testValue(17, new byte[]{17}, 1); + } + + @Test + public void test0x80() { + // This is 1 << 8, the first 'overflowing' value + testValue(0x80, new byte[]{(byte) 0x81, 0x00}, 2); + } + + @Test + public void testDoubleValue() { + testValue(460, new byte[]{(byte) 0x83, 0x4C}, 2); + } + + @Test + public void testOffset() { + int intValue = 460; + byte[] bytesValue = {(byte) 0x83, 0x4C}; + byte[] bytesTest = {0x00, (byte) 0x83, 0x4C}; + int size = 2; + + Assert.assertEquals(size, VarInt.getVarIntSize(intValue)); + Assert.assertEquals(intValue, VarInt.getVarIntValue(bytesTest, 1)); + Assert.assertArrayEquals(bytesValue, VarInt.putVarIntValue(intValue)); + + VarInt varInt = new VarInt(bytesTest, 1); + Assert.assertEquals(size, varInt.size); + Assert.assertEquals(intValue, varInt.dValue); + Assert.assertArrayEquals(bytesValue, varInt.eValue); + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestAlarms.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestAlarms.java new file mode 100644 index 000000000..0712dd247 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestAlarms.java @@ -0,0 +1,167 @@ +/* Copyright (C) 2022-2023 Martin.JM + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV; + +import org.junit.Assert; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.util.List; + +public class TestAlarms { + + HuaweiPacket.ParamsProvider paramsProvider = new HuaweiPacket.ParamsProvider() { + @Override + public byte getDeviceSupportType() { + return 0; + } + + @Override + public byte[] getSecretKey() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public byte[] getIv() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public boolean areTransactionsCrypted() { + return true; + } + + @Override + public int getMtu() { + return 0; + } + + @Override + public int getSliceSize() { + return 0xF4; + } + }; + + @Test + public void testEventAlarmsRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + Field alarmField = Alarms.EventAlarmsRequest.class.getDeclaredField("alarms"); + alarmField.setAccessible(true); + + HuaweiTLV expectedAlarmsTlv = new HuaweiTLV(); + + Alarms.EventAlarmsRequest request = new Alarms.EventAlarmsRequest(paramsProvider); + + expectedAlarmsTlv.put(0x82, new HuaweiTLV() + .put(0x03, (byte) 1) + .put(0x04, true) + .put(0x05, (short) 0x1337) + .put(0x06, (byte) 0) + .put(0x07, "Alarm1") + ); + + request.addEventAlarm( + new Alarms.EventAlarm( + (byte) 1, + true, + (byte) 0x13, + (byte) 0x37, + (byte) 0, + "Alarm1" + ) + ); + + Assert.assertEquals(0x08, request.serviceId); + Assert.assertEquals(0x01, request.commandId); + // TODO: check count in request + Assert.assertEquals(expectedAlarmsTlv, alarmField.get(request)); + + // A serialize will change the tlv, so we cannot test it here + + expectedAlarmsTlv.put(0x82,new HuaweiTLV() + .put(0x03, (byte) 2) + .put(0x04, false) + .put(0x05, (short) 0xCAFE) + .put(0x06, (byte) 1) + .put(0x07, "Alarm2") + ); + + request.addEventAlarm( + new Alarms.EventAlarm( + (byte) 2, + false, + (byte) 0xCA, + (byte) 0xFE, + (byte) 1, + "Alarm2" + ) + ); + + Assert.assertEquals(expectedAlarmsTlv, alarmField.get(request)); + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x81, expectedAlarmsTlv); + + // Different order for better assertion messages in case of failure + List listOut = request.serialize(); + Assert.assertEquals(1, listOut.size()); + Assert.assertEquals(expectedAlarmsTlv, alarmField.get(request)); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + } + + @Test + public void testSmartAlarmRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + byte[] expectedOutput = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x3a, (byte) 0x00, (byte) 0x08, (byte) 0x02, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x20, (byte) 0xcd, (byte) 0x7f, (byte) 0x80, (byte) 0x67, (byte) 0x02, (byte) 0x8d, (byte) 0x46, (byte) 0xfb, (byte) 0xc1, (byte) 0x0b, (byte) 0xed, (byte) 0x6c, (byte) 0x46, (byte) 0xb7, (byte) 0x59, (byte) 0xba, (byte) 0x08, (byte) 0xfd, (byte) 0xde, (byte) 0x3b, (byte) 0xee, (byte) 0x54, (byte) 0xbd, (byte) 0x4f, (byte) 0x27, (byte) 0xf6, (byte) 0x52, (byte) 0x9a, (byte) 0xae, (byte) 0xbf, (byte) 0x55, (byte) 0xd9, (byte) 0xe0, (byte) 0xa6}; + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x81, new HuaweiTLV() + .put(0x82, new HuaweiTLV() + .put(0x03, (byte) 0x01) + .put(0x04, true) + .put(0x05, (short) 0x1337) + .put(0x06, (byte) 1) + .put(0x07, (byte) 2) + ) + ); + + Alarms.SmartAlarmRequest request = new Alarms.SmartAlarmRequest( + paramsProvider, + new Alarms.SmartAlarm( + true, + (byte) 0x13, + (byte) 0x37, + (byte) 1, + (byte) 2 + ) + ); + + Assert.assertEquals(0x08, request.serviceId); + Assert.assertEquals(0x02, request.commandId); + Assert.assertTrue(request.complete); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + List out = request.serialize(); + Assert.assertEquals(1, out.size()); + Assert.assertArrayEquals(expectedOutput, out.get(0)); + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestCalls.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestCalls.java new file mode 100644 index 000000000..366a19d05 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestCalls.java @@ -0,0 +1,68 @@ +/* Copyright (C) 2022 Martin.JM + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets; + +import org.junit.Assert; +import org.junit.Test; + +import java.lang.reflect.Field; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV; + +public class TestCalls { + + @Test + public void testAnswerCallResponseAccept() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] raw = new byte[] {(byte) 0x5A, (byte) 0x00, (byte) 0x06, (byte) 0x00, (byte) 0x04, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x02, (byte) 0x99, (byte) 0x6B}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x01, (byte) 0x02); + + HuaweiPacket packet = new HuaweiPacket(null).parse(raw); + packet.parseTlv(); + + Assert.assertEquals(0x04, packet.serviceId); + Assert.assertEquals(0x01, packet.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packet)); + Assert.assertTrue(packet.complete); + Assert.assertTrue(packet instanceof Calls.AnswerCallResponse); + Assert.assertEquals(Calls.AnswerCallResponse.Action.CALL_ACCEPT, ((Calls.AnswerCallResponse) packet).action); + } + + @Test + public void testAnswerCallResponseReject() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] raw = new byte[] {(byte) 0x5A, (byte) 0x00, (byte) 0x06, (byte) 0x00, (byte) 0x04, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0xA9, (byte) 0x08}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x01, (byte) 0x01); + + HuaweiPacket packet = new HuaweiPacket(null).parse(raw); + packet.parseTlv(); + + Assert.assertEquals(0x04, packet.serviceId); + Assert.assertEquals(0x01, packet.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packet)); + Assert.assertTrue(packet.complete); + Assert.assertTrue(packet instanceof Calls.AnswerCallResponse); + Assert.assertEquals(Calls.AnswerCallResponse.Action.CALL_REJECT, ((Calls.AnswerCallResponse) packet).action); + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestDeviceConfig.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestDeviceConfig.java new file mode 100644 index 000000000..d8d48c180 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestDeviceConfig.java @@ -0,0 +1,505 @@ +/* Copyright (C) 2022 Gaignon Damien, Martin.JM + Copyright (C) 2022-2023 MartinJM + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCoordinatorSupplier; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCrypto; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV; + +import org.junit.Assert; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.TimeZone; + +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; + +public class TestDeviceConfig { + + HuaweiPacket.ParamsProvider secretsProvider = new HuaweiPacket.ParamsProvider() { + @Override + public byte getDeviceSupportType() { + return 0; + } + + @Override + public byte[] getSecretKey() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public byte[] getIv() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public boolean areTransactionsCrypted() { + return true; + } + + @Override + public int getMtu() { + return 0; + } + + @Override + public int getSliceSize() { + return 0xF4; + } + }; + + @Test + public void testLinkParamsRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x01) + .put(0x02) + .put(0x03) + .put(0x04); + + byte[] serialized = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x0b, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x02, (byte) 0x00, (byte) 0x03, (byte) 0x00, (byte) 0x04, (byte) 0x00, (byte) 0xf1, (byte) 0x3b}; + DeviceConfig.LinkParams.Request request = new DeviceConfig.LinkParams.Request( + secretsProvider, HuaweiCoordinatorSupplier.HuaweiDeviceType.BLE + ); + + Assert.assertEquals(0x01, request.serviceId); + Assert.assertEquals(0x01, request.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + Assert.assertTrue(request.complete); + List out = request.serialize(); + Assert.assertEquals(1, out.size()); + Assert.assertArrayEquals(serialized, out.get(0)); + } + + @Test + public void testLinkParamsResponse() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] rawLinkParams = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x1b, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x03, (byte) 0x02, (byte) 0x00, (byte) 0x14, (byte) 0x05, (byte) 0x12, (byte) 0x00, (byte) 0x01, (byte) 0x6a, (byte) 0xa2, (byte) 0x96, (byte) 0xe3, (byte) 0x76, (byte) 0x41, (byte) 0xb1, (byte) 0x0c, (byte) 0xf8, (byte) 0xaa, (byte) 0xf7, (byte) 0x47, (byte) 0x05, (byte) 0x5d, (byte) 0x0a, (byte) 0xa3, (byte) 0xe8, (byte) 0x9f}; + + byte[] expectedServerNonceWithAuth = new byte[] {(byte) 0x00, (byte) 0x01, (byte) 0x6A, (byte) 0xA2, (byte) 0x96, (byte) 0xE3, (byte) 0x76, (byte) 0x41, (byte) 0xB1, (byte) 0x0C, (byte) 0xF8, (byte) 0xAA, (byte) 0xF7, (byte) 0x47, (byte) 0x05, (byte) 0x5D, (byte) 0x0A, (byte) 0xA3}; + byte[] expectedServerNonce = new byte[expectedServerNonceWithAuth.length - 2]; + System.arraycopy(expectedServerNonceWithAuth, 2, expectedServerNonce, 0, expectedServerNonceWithAuth.length - 2); + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x03, new byte[] {0x00, 0x14}) + .put(0x05, expectedServerNonceWithAuth); + + HuaweiPacket packetLinkParams = new HuaweiPacket(secretsProvider).parse(rawLinkParams); + packetLinkParams.parseTlv(); + + Assert.assertEquals(0x01, packetLinkParams.serviceId); + Assert.assertEquals(0x01, packetLinkParams.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packetLinkParams)); + Assert.assertTrue(packetLinkParams.complete); + Assert.assertTrue(packetLinkParams instanceof DeviceConfig.LinkParams.Response); + Assert.assertEquals(0x01, ((DeviceConfig.LinkParams.Response) packetLinkParams).authVersion); + Assert.assertArrayEquals(expectedServerNonce, ((DeviceConfig.LinkParams.Response) packetLinkParams).serverNonce); + } + + @Test(expected=HuaweiPacket.ParseException.class) + public void testLinkParamsResponseException() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] rawLinkParams = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x17, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x12, (byte) 0x00, (byte) 0x01, (byte) 0x6a, (byte) 0xa2, (byte) 0x96, (byte) 0xe3, (byte) 0x76, (byte) 0x41, (byte) 0xb1, (byte) 0x0c, (byte) 0xf8, (byte) 0xaa, (byte) 0xf7, (byte) 0x47, (byte) 0x05, (byte) 0x5d, (byte) 0x0a, (byte) 0xa3, (byte) 0xdd, (byte) 0x41}; + + HuaweiPacket packetLinkParams = new HuaweiPacket(secretsProvider).parse(rawLinkParams); + packetLinkParams.parseTlv(); + } + + @Test + public void testSupportedServicesRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + byte[] allSupportedServices = new byte[] {(byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04, (byte) 0x05, (byte) 0x06, (byte) 0x07, (byte) 0x08, (byte) 0x09, (byte) 0x0a, (byte) 0x0b, (byte) 0x0c, (byte) 0x0d, (byte) 0x0e, (byte) 0x0f, (byte) 0x10, (byte) 0x11, (byte) 0x12, (byte) 0x13, (byte) 0x14, (byte) 0x15, (byte) 0x16, (byte) 0x17, (byte) 0x18, (byte) 0x19, (byte) 0x1a, (byte) 0x1b, (byte) 0x1c, (byte) 0x1d, (byte) 0x1e, (byte) 0x1f, (byte) 0x20, (byte) 0x21, (byte) 0x22, (byte) 0x23, (byte) 0x24, (byte) 0x25, (byte) 0x26, (byte) 0x27, (byte) 0x28, (byte) 0x29, (byte) 0x2a, (byte) 0x2b, (byte) 0x2c, (byte) 0x2d, (byte) 0x2e, (byte) 0x2f, (byte) 0x30, (byte) 0x31, (byte) 0x32, (byte) 0x33, (byte) 0x34, (byte) 0x35}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x01, allSupportedServices); + + byte[] serialized = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x5a, (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x40, (byte) 0x14, (byte) 0xb1, (byte) 0x75, (byte) 0x7d, (byte) 0xc0, (byte) 0xa5, (byte) 0x32, (byte) 0xeb, (byte) 0xc1, (byte) 0x20, (byte) 0x7b, (byte) 0xb8, (byte) 0x59, (byte) 0xdb, (byte) 0xdb, (byte) 0xfe, (byte) 0x5b, (byte) 0x01, (byte) 0x0a, (byte) 0x7d, (byte) 0xb7, (byte) 0x76, (byte) 0xfc, (byte) 0xcc, (byte) 0x5f, (byte) 0x22, (byte) 0xff, (byte) 0x13, (byte) 0xcb, (byte) 0xbb, (byte) 0x4f, (byte) 0xe2, (byte) 0xcd, (byte) 0x6e, (byte) 0x4b, (byte) 0xd7, (byte) 0x7c, (byte) 0x05, (byte) 0x24, (byte) 0x85, (byte) 0x65, (byte) 0x5f, (byte) 0x95, (byte) 0x32, (byte) 0xb4, (byte) 0x5e, (byte) 0x16, (byte) 0xef, (byte) 0xad, (byte) 0x62, (byte) 0x38, (byte) 0xd5, (byte) 0x88, (byte) 0x63, (byte) 0xa4, (byte) 0xb0, (byte) 0x29, (byte) 0xbb, (byte) 0x90, (byte) 0x66, (byte) 0x8c, (byte) 0x3f, (byte) 0x58, (byte) 0x69, (byte) 0x40, (byte) 0x22}; + DeviceConfig.SupportedServices.Request request = new DeviceConfig.SupportedServices.Request( + secretsProvider, + allSupportedServices + ); + + Assert.assertEquals(0x01, request.serviceId); + Assert.assertEquals(0x02, request.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + Assert.assertTrue(request.complete); + List out = request.serialize(); + Assert.assertEquals(1, out.size()); + Assert.assertArrayEquals(serialized, out.get(0)); + } + + @Test + public void testSupportedServicesResponse() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] rawSupportedServices = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x5a, (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x40, (byte) 0xC8, (byte) 0x9F, (byte) 0x1E, (byte) 0x2F, (byte) 0xE8, (byte) 0x31, (byte) 0xC8, (byte) 0x1E, (byte) 0x92, (byte) 0xB0, (byte) 0xE8, (byte) 0x9E, (byte) 0xC7, (byte) 0x2E, (byte) 0x76, (byte) 0xD7, (byte) 0x6C, (byte) 0x64, (byte) 0x22, (byte) 0x5A, (byte) 0x6C, (byte) 0xF9, (byte) 0xAF, (byte) 0xFB, (byte) 0x8E, (byte) 0x98, (byte) 0x74, (byte) 0xB6, (byte) 0xF9, (byte) 0x84, (byte) 0x3C, (byte) 0x1E, (byte) 0x3D, (byte) 0xCB, (byte) 0x7C, (byte) 0x23, (byte) 0x4F, (byte) 0x7B, (byte) 0x34, (byte) 0x0C, (byte) 0x49, (byte) 0xBD, (byte) 0x80, (byte) 0x94, (byte) 0x67, (byte) 0x1B, (byte) 0x5C, (byte) 0x64, (byte) 0x6B, (byte) 0xA4, (byte) 0xB9, (byte) 0xEC, (byte) 0xA7, (byte) 0x97, (byte) 0x95, (byte) 0x6F, (byte) 0x44, (byte) 0x13, (byte) 0x66, (byte) 0x7C, (byte) 0xF5, (byte) 0x9F, (byte) 0x05, (byte) 0x72, (byte) 0xc9, (byte) 0xe9}; + byte[] expectedSupportedServices = new byte[] {(byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x02, expectedSupportedServices); + + HuaweiPacket packetSupportedServices = new HuaweiPacket(secretsProvider).parse(rawSupportedServices); + packetSupportedServices.parseTlv(); + + Assert.assertEquals(0x01, packetSupportedServices.serviceId); + Assert.assertEquals(0x02, packetSupportedServices.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packetSupportedServices)); + Assert.assertTrue(packetSupportedServices.complete); + Assert.assertTrue(packetSupportedServices instanceof DeviceConfig.SupportedServices.Response); + Assert.assertArrayEquals(expectedSupportedServices, ((DeviceConfig.SupportedServices.Response) packetSupportedServices).supportedServices); + } + + @Test(expected=HuaweiPacket.ParseException.class) + public void testSupportedServicesResponseException() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] rawSupportedServices = new byte[] {(byte) 0x5A, (byte) 0x00, (byte) 0x5A, (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x7C, (byte) 0x01, (byte) 0x01, (byte) 0x7D, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7E, (byte) 0x40, (byte) 0x28, (byte) 0xB0, (byte) 0x4D, (byte) 0x00, (byte) 0xCF, (byte) 0xE1, (byte) 0xD1, (byte) 0x7A, (byte) 0x5D, (byte) 0xE7, (byte) 0x61, (byte) 0x0E, (byte) 0xE1, (byte) 0xA5, (byte) 0xE8, (byte) 0xF9, (byte) 0x6D, (byte) 0x2D, (byte) 0x32, (byte) 0xB3, (byte) 0xC3, (byte) 0x7C, (byte) 0x07, (byte) 0xBC, (byte) 0x11, (byte) 0x03, (byte) 0x8A, (byte) 0x66, (byte) 0x8C, (byte) 0x47, (byte) 0x94, (byte) 0x86, (byte) 0x8C, (byte) 0x0D, (byte) 0xC6, (byte) 0xBC, (byte) 0xDF, (byte) 0xB3, (byte) 0x00, (byte) 0xFB, (byte) 0x68, (byte) 0x11, (byte) 0xC1, (byte) 0xB3, (byte) 0x66, (byte) 0x6D, (byte) 0x85, (byte) 0x6F, (byte) 0xF0, (byte) 0xA9, (byte) 0xD0, (byte) 0x49, (byte) 0xDF, (byte) 0xF5, (byte) 0x82, (byte) 0x01, (byte) 0x9F, (byte) 0xE4, (byte) 0x60, (byte) 0x36, (byte) 0x81, (byte) 0xAA, (byte) 0x31, (byte) 0xA1, (byte) 0x39, (byte) 0xD6}; + + HuaweiPacket packetSupportedServices = new HuaweiPacket(secretsProvider).parse(rawSupportedServices); + packetSupportedServices.parseTlv(); + } + + @Test + public void testSupportedCommandsRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + byte service1 = (byte) 0x01; + byte[] commands1 = new byte[]{(byte) 0x04, (byte) 0x07, (byte) 0x08, (byte) 0x09, (byte) 0x0A, (byte) 0x0D, (byte) 0x0E, (byte) 0x10, (byte) 0x11, (byte) 0x12, (byte) 0x13, (byte) 0x14, (byte) 0x1B, (byte) 0x1A, (byte) 0x1D, (byte) 0x21, (byte) 0x22, (byte) 0x23, (byte) 0x24, (byte) 0x29, (byte) 0x2A, (byte) 0x2B, (byte) 0x32, (byte) 0x2E, (byte) 0x31, (byte) 0x30, (byte) 0x35, (byte) 0x36, (byte) 0x37, (byte) 0x2F}; + byte service2 = (byte) 0x02; + byte[] commands2 = new byte[]{(byte) 0x01, (byte) 0x04, (byte) 0x05, (byte) 0x06, (byte) 0x07, (byte) 0x08}; + byte service3 = (byte) 0x04; + byte[] commands3 = new byte[]{(byte) 0x01}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x81, new HuaweiTLV() + .put(0x02, service1) + .put(0x03, commands1) + .put(0x02, service2) + .put(0x03, commands2) + .put(0x02, service3) + .put(0x03, commands3) + ); + + byte[] expectedSerialized = new byte[] {(byte) 0x5A, (byte) 0x00, (byte) 0x5A, (byte) 0x00, (byte) 0x01, (byte) 0x03, (byte) 0x7C, (byte) 0x01, (byte) 0x01, (byte) 0x7D, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7E, (byte) 0x40, (byte) 0x81, (byte) 0xED, (byte) 0x07, (byte) 0xF6, (byte) 0x51, (byte) 0xA3, (byte) 0x19, (byte) 0xBD, (byte) 0xCE, (byte) 0x35, (byte) 0x13, (byte) 0x23, (byte) 0x1E, (byte) 0xFC, (byte) 0x1A, (byte) 0x51, (byte) 0x92, (byte) 0xBB, (byte) 0x43, (byte) 0xC5, (byte) 0xF5, (byte) 0xD9, (byte) 0x4E, (byte) 0xCC, (byte) 0x2F, (byte) 0xE0, (byte) 0xDB, (byte) 0xB1, (byte) 0x5E, (byte) 0x78, (byte) 0x66, (byte) 0x69, (byte) 0x61, (byte) 0x85, (byte) 0x46, (byte) 0xB2, (byte) 0x50, (byte) 0xEC, (byte) 0xB5, (byte) 0x3F, (byte) 0x74, (byte) 0x68, (byte) 0x47, (byte) 0x03, (byte) 0x87, (byte) 0xC1, (byte) 0xB3, (byte) 0x53, (byte) 0x7B, (byte) 0x53, (byte) 0xDB, (byte) 0xE8, (byte) 0x5E, (byte) 0x82, (byte) 0x56, (byte) 0xFD, (byte) 0x16, (byte) 0x66, (byte) 0x03, (byte) 0xB2, (byte) 0x56, (byte) 0xA3, (byte) 0x14, (byte) 0x70, (byte) 0x38, (byte) 0x3E}; + DeviceConfig.SupportedCommands.Request request = new DeviceConfig.SupportedCommands.Request( + secretsProvider + ); + request.addCommandsForService(service1, commands1); + request.addCommandsForService(service2, commands2); + request.addCommandsForService(service3, commands3); + + List out = request.serialize(); + + Assert.assertEquals(0x01, request.serviceId); + Assert.assertEquals(0x03, request.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + Assert.assertTrue(request.complete); + Assert.assertEquals(1, out.size()); + Assert.assertArrayEquals(expectedSerialized, out.get(0)); + } + + @Test + public void testSupportedCommandsResponse() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] rawSupportedCommands = new byte[] {(byte) 0x5A, (byte) 0x00, (byte) 0x5A, (byte) 0x00, (byte) 0x01, (byte) 0x03, (byte) 0x7C, (byte) 0x01, (byte) 0x01, (byte) 0x7D, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7E, (byte) 0x40, (byte) 0x8B, (byte) 0x86, (byte) 0x29, (byte) 0xE3, (byte) 0x90, (byte) 0x25, (byte) 0x4E, (byte) 0x14, (byte) 0xE1, (byte) 0xDD, (byte) 0x96, (byte) 0x63, (byte) 0x66, (byte) 0xB8, (byte) 0x1E, (byte) 0x4A, (byte) 0xC1, (byte) 0xA7, (byte) 0x49, (byte) 0xB0, (byte) 0x9F, (byte) 0x21, (byte) 0x7C, (byte) 0xE8, (byte) 0x2C, (byte) 0x72, (byte) 0x93, (byte) 0x9F, (byte) 0xAC, (byte) 0x37, (byte) 0x3B, (byte) 0x4D, (byte) 0x1A, (byte) 0xCB, (byte) 0xC2, (byte) 0xFF, (byte) 0x64, (byte) 0xE5, (byte) 0xF0, (byte) 0x3E, (byte) 0x5B, (byte) 0xFF, (byte) 0xB1, (byte) 0x9C, (byte) 0x59, (byte) 0xB2, (byte) 0xF1, (byte) 0xD6, (byte) 0x4B, (byte) 0x2B, (byte) 0x99, (byte) 0xFB, (byte) 0xEA, (byte) 0x29, (byte) 0x66, (byte) 0xD3, (byte) 0x90, (byte) 0x0B, (byte) 0xC9, (byte) 0xF0, (byte) 0xB4, (byte) 0x9B, (byte) 0x3B, (byte) 0x3E, (byte) 0x50, (byte) 0xFA}; + + List expectedSupportedCommandsList = new ArrayList<>(); + DeviceConfig.SupportedCommands.Response.CommandsList commandsList = new DeviceConfig.SupportedCommands.Response.CommandsList(); + commandsList.service = 1; + commandsList.commands = new byte[] {(byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04, (byte) 0x05, (byte) 0x06, (byte) 0x07, (byte) 0x09, (byte) 0x0A, (byte) 0x0B, (byte) 0x0D, (byte) 0x0F, (byte) 0x10, (byte) 0x11}; + expectedSupportedCommandsList.add(commandsList); + commandsList.service = 2; + commandsList.commands = new byte[] {(byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04, (byte) 0x06}; + expectedSupportedCommandsList.add(commandsList); + commandsList.service = 4; + commandsList.commands = new byte[] {(byte) 0x01}; + expectedSupportedCommandsList.add(commandsList); + + byte[] expectedSupportedCommands = new byte[] {(byte) 0x02, (byte) 0x01, (byte) 0x01, (byte) 0x04, (byte) 0x1E, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x02, (byte) 0x01, (byte) 0x02, (byte) 0x04, (byte) 0x06, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x01, (byte) 0x04, (byte) 0x04, (byte) 0x01, (byte) 0x01}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x81, expectedSupportedCommands); + + HuaweiPacket packetSupportedCommands = new HuaweiPacket(secretsProvider).parse(rawSupportedCommands); + packetSupportedCommands.parseTlv(); + + Assert.assertEquals(0x01, packetSupportedCommands.serviceId); + Assert.assertEquals(0x03, packetSupportedCommands.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packetSupportedCommands)); + Assert.assertTrue(packetSupportedCommands.complete); + Assert.assertTrue(packetSupportedCommands instanceof DeviceConfig.SupportedCommands.Response); + Assert.assertEquals(expectedSupportedCommandsList.size(), ((DeviceConfig.SupportedCommands.Response) packetSupportedCommands).commandsLists.size()); + } + + @Test(expected=HuaweiPacket.ParseException.class) + public void testSupportedCommandsResponseException02() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] rawSupportedCommands = new byte[] {(byte) 0x5A, (byte) 0x00, (byte) 0x4A, (byte) 0x00, (byte) 0x01, (byte) 0x03, (byte) 0x7C, (byte) 0x01, (byte) 0x01, (byte) 0x7D, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7E, (byte) 0x30, (byte) 0x57, (byte) 0xC7, (byte) 0xF7, (byte) 0x47, (byte) 0x0E, (byte) 0xA9, (byte) 0xA2, (byte) 0x9E, (byte) 0xF3, (byte) 0xB0, (byte) 0xBD, (byte) 0x02, (byte) 0xE0, (byte) 0x79, (byte) 0x3C, (byte) 0x12, (byte) 0xE6, (byte) 0x58, (byte) 0xDA, (byte) 0xF7, (byte) 0x0B, (byte) 0xC3, (byte) 0x93, (byte) 0x8D, (byte) 0x37, (byte) 0x2E, (byte) 0xA9, (byte) 0xB8, (byte) 0xF8, (byte) 0xF7, (byte) 0x97, (byte) 0xF3, (byte) 0x22, (byte) 0x08, (byte) 0xDF, (byte) 0xAD, (byte) 0x2B, (byte) 0x62, (byte) 0x33, (byte) 0x11, (byte) 0x93, (byte) 0x66, (byte) 0xD1, (byte) 0xAE, (byte) 0xF3, (byte) 0x02, (byte) 0x18, (byte) 0x49, (byte) 0xD0, (byte) 0x40}; + + HuaweiPacket packetSupportedCommands = new HuaweiPacket(secretsProvider).parse(rawSupportedCommands); + packetSupportedCommands.parseTlv(); + } + + @Test(expected=HuaweiPacket.ParseException.class) + public void testSupportedCommandsResponseException04() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] rawSupportedCommands = new byte[] {(byte) 0x5A, (byte) 0x00, (byte) 0x2A, (byte) 0x00, (byte) 0x01, (byte) 0x03, (byte) 0x7C, (byte) 0x01, (byte) 0x01, (byte) 0x7D, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7E, (byte) 0x10, (byte) 0x25, (byte) 0x26, (byte) 0xE0, (byte) 0x79, (byte) 0x1D, (byte) 0x19, (byte) 0x82, (byte) 0xDB, (byte) 0x0A, (byte) 0x3A, (byte) 0x21, (byte) 0x6E, (byte) 0x70, (byte) 0x52, (byte) 0xAB, (byte) 0xF3, (byte) 0x14, (byte) 0xB4}; + + HuaweiPacket packetSupportedCommands = new HuaweiPacket(secretsProvider).parse(rawSupportedCommands); + packetSupportedCommands.parseTlv(); + } + + @Test + public void testDateFormatRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + byte dateformat = (byte) 0x02; + byte timeFormat = (byte) 0x02; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x81, new HuaweiTLV() + .put(0x02, dateformat) + .put(0x03, timeFormat) + ); + + byte[] serialized = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x01, (byte) 0x04, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0x63, (byte) 0xe0, (byte) 0xf1, (byte) 0xbf, (byte) 0x40, (byte) 0xab, (byte) 0x09, (byte) 0x63, (byte) 0x51, (byte) 0x7c, (byte) 0xa7, (byte) 0x8c, (byte) 0x2e, (byte) 0xd9, (byte) 0x6a, (byte) 0x6c, (byte) 0xdc, (byte) 0xe9}; + DeviceConfig.DateFormat.Request request = new DeviceConfig.DateFormat.Request ( + secretsProvider, + dateformat, + timeFormat + ); + + Assert.assertEquals(0x01, request.serviceId); + Assert.assertEquals(0x04, request.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + Assert.assertTrue(request.complete); + List out = request.serialize(); + Assert.assertEquals(1, out.size()); + Assert.assertArrayEquals(serialized, out.get(0)); + } + + @Test + public void testTimeRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + int timestamp = 1633987331; + short zoneOffset = (short) 512; + + Calendar calendar = Calendar.getInstance(); + calendar.setTimeInMillis(timestamp * 1000L); + calendar.setTimeZone(TimeZone.getTimeZone("GMT+2")); + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x01, timestamp) + .put(0x02, zoneOffset); + + byte[] serialized = new byte[] {(byte) 0x5A, (byte) 0x00, (byte) 0x2A, (byte) 0x00, (byte) 0x01, (byte) 0x05, (byte) 0x7C, (byte) 0x01, (byte) 0x01, (byte) 0x7D, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7E, (byte) 0x10, (byte) 0xED, (byte) 0x67, (byte) 0x61, (byte) 0x8A, (byte) 0x8E, (byte) 0x44, (byte) 0x67, (byte) 0xB1, (byte) 0x2A, (byte) 0xB4, (byte) 0xFA, (byte) 0x86, (byte) 0x76, (byte) 0x17, (byte) 0x8C, (byte) 0x61, (byte) 0xFC, (byte) 0x99}; + DeviceConfig.TimeRequest request = new DeviceConfig.TimeRequest(secretsProvider, calendar); + + Assert.assertEquals(0x01, request.serviceId); + Assert.assertEquals(0x05, request.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + Assert.assertTrue(request.complete); + List out = request.serialize(); + Assert.assertEquals(1, out.size()); + Assert.assertArrayEquals(serialized, out.get(0)); + } + + @Test + public void testProductInformationRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV(); + byte[] expectedTags = {0x01, 0x02, 0x07, 0x09, 0x0A, 0x11, 0x12, 0x16, 0x1A, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23}; + for (byte tag : expectedTags) { + expectedTlv.put(tag); + } + + // Outdated + //byte[] serialized = new byte[] {(byte) 0x5A, (byte) 0x00, (byte) 0x3A, (byte) 0x00, (byte) 0x01, (byte) 0x07, (byte) 0x7C, (byte) 0x01, (byte) 0x01, (byte) 0x7D, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7E, (byte) 0x20, (byte) 0x10, (byte) 0x9B, (byte) 0x27, (byte) 0x5D, (byte) 0xB1, (byte) 0x3C, (byte) 0xFD, (byte) 0x40, (byte) 0x4B, (byte) 0xA8, (byte) 0xAC, (byte) 0xAF, (byte) 0x8A, (byte) 0xB6, (byte) 0xA5, (byte) 0x3D, (byte) 0x40, (byte) 0x30, (byte) 0x2C, (byte) 0x79, (byte) 0x98, (byte) 0x6D, (byte) 0xEC, (byte) 0xD1, (byte) 0x39, (byte) 0xE6, (byte) 0xFE, (byte) 0x5C, (byte) 0xE8, (byte) 0xB2, (byte) 0xF3, (byte) 0x9E, (byte) 0x3E, (byte) 0x1B}; + DeviceConfig.ProductInfo.Request request = new DeviceConfig.ProductInfo.Request ( + secretsProvider, HuaweiCoordinatorSupplier.HuaweiDeviceType.BLE + ); + + Assert.assertEquals(0x01, request.serviceId); + Assert.assertEquals(0x07, request.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + Assert.assertTrue(request.complete); + List out = request.serialize(); + Assert.assertEquals(1, out.size()); + // Assert.assertArrayEquals(serialized, out.get(0)); + } + + @Test + public void testProductInformationResponse() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] rawProductInformation = new byte[] {(byte) 0x5A, (byte) 0x00, (byte) 0x4A, (byte) 0x00, (byte) 0x01, (byte) 0x07, (byte) 0x7C, (byte) 0x01, (byte) 0x01, (byte) 0x7D, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7E, (byte) 0x30, (byte) 0x50, (byte) 0x75, (byte) 0xA5, (byte) 0x4F, (byte) 0x26, (byte) 0xF7, (byte) 0x74, (byte) 0x0B, (byte) 0xB2, (byte) 0xD8, (byte) 0x01, (byte) 0xBA, (byte) 0xDC, (byte) 0x7E, (byte) 0x40, (byte) 0x36, (byte) 0xD5, (byte) 0x6D, (byte) 0x4B, (byte) 0x7B, (byte) 0x8F, (byte) 0xC6, (byte) 0xFB, (byte) 0x48, (byte) 0xFC, (byte) 0x89, (byte) 0x54, (byte) 0xF8, (byte) 0xBB, (byte) 0xC0, (byte) 0x48, (byte) 0x9E, (byte) 0x34, (byte) 0x0E, (byte) 0xB1, (byte) 0x24, (byte) 0xD8, (byte) 0x89, (byte) 0x02, (byte) 0x7E, (byte) 0x6C, (byte) 0x3E, (byte) 0x81, (byte) 0x7D, (byte) 0x38, (byte) 0x0F, (byte) 0xD9, (byte) 0x2A, (byte) 0x98, (byte) 0xE3}; + String softwareVersion = "1.0.10.78"; + String productModel = "Crius"; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x03, new byte[] {(byte) 0x4E, (byte) 0x41}) + .put(0x07, new byte[] {(byte) 0x31, (byte) 0x2E, (byte) 0x30, (byte) 0x2E, (byte) 0x31, (byte) 0x30, (byte) 0x2E, (byte) 0x37, (byte) 0x38}) + .put(0x0A, new byte[] {(byte) 0x43, (byte) 0x72, (byte) 0x69, (byte) 0x75, (byte) 0x73, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00}); + + HuaweiPacket packetProductInformation = new HuaweiPacket(secretsProvider).parse(rawProductInformation); + packetProductInformation.parseTlv(); + + Assert.assertEquals(0x01, packetProductInformation.serviceId); + Assert.assertEquals(0x07, packetProductInformation.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packetProductInformation)); + Assert.assertTrue(packetProductInformation.complete); + Assert.assertTrue(packetProductInformation instanceof DeviceConfig.ProductInfo.Response); + Assert.assertTrue(softwareVersion.equals(((DeviceConfig.ProductInfo.Response) packetProductInformation).softwareVersion)); + System.out.println(((DeviceConfig.ProductInfo.Response) packetProductInformation).productModel); + System.out.println(((DeviceConfig.ProductInfo.Response) packetProductInformation).productModel.length()); + Assert.assertTrue(productModel.equals(((DeviceConfig.ProductInfo.Response) packetProductInformation).productModel)); + } + + @Test(expected=HuaweiPacket.ParseException.class) + public void testProductInformationResponseException07() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] rawProductInformation = new byte[] {(byte) 0x5A, (byte) 0x00, (byte) 0x3A, (byte) 0x00, (byte) 0x01, (byte) 0x07, (byte) 0x7C, (byte) 0x01, (byte) 0x01, (byte) 0x7D, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7E, (byte) 0x20, (byte) 0xFE, (byte) 0x24, (byte) 0x4C, (byte) 0x68, (byte) 0xFC, (byte) 0x1D, (byte) 0xAD, (byte) 0x64, (byte) 0x77, (byte) 0xC9, (byte) 0xE9, (byte) 0x26, (byte) 0x8D, (byte) 0x3C, (byte) 0x3C, (byte) 0x8C, (byte) 0xB6, (byte) 0xA6, (byte) 0xF1, (byte) 0xBF, (byte) 0xAC, (byte) 0xB6, (byte) 0x7A, (byte) 0x75, (byte) 0xA3, (byte) 0xA9, (byte) 0x07, (byte) 0x5F, (byte) 0x39, (byte) 0x0F, (byte) 0x28, (byte) 0x61, (byte) 0x50, (byte) 0x61}; + + HuaweiPacket packetProductInformation = new HuaweiPacket(secretsProvider).parse(rawProductInformation); + packetProductInformation.parseTlv(); + } + + @Test(expected=HuaweiPacket.ParseException.class) + public void testProductInformationResponseException0A() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] rawProductInformation = new byte[] {(byte) 0x5A, (byte) 0x00, (byte) 0x2A, (byte) 0x00, (byte) 0x01, (byte) 0x07, (byte) 0x7C, (byte) 0x01, (byte) 0x01, (byte) 0x7D, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7E, (byte) 0x10, (byte) 0xB6, (byte) 0x67, (byte) 0xA5, (byte) 0x6A, (byte) 0x46, (byte) 0x0F, (byte) 0x08, (byte) 0x1E, (byte) 0xAC, (byte) 0x1E, (byte) 0x6B, (byte) 0xF2, (byte) 0x11, (byte) 0x4A, (byte) 0x54, (byte) 0x20, (byte) 0xCF, (byte) 0xB6}; + + HuaweiPacket packetProductInformation = new HuaweiPacket(secretsProvider).parse(rawProductInformation); + packetProductInformation.parseTlv(); + } + + @Test + public void testBondRequest() throws NoSuchFieldException, IllegalAccessException { + byte[] clientSerial = new byte[] {(byte) 0x54, (byte) 0x56, (byte) 0x64, (byte) 0x54, (byte) 0x4D, (byte) 0x44}; + String mac = "FF:FF:FF:FF:FF:CC"; + HuaweiCrypto huaweiCrypto = new HuaweiCrypto(0x01); + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + try { + byte[] encryptionKey = huaweiCrypto.createSecretKey(mac); + byte[] iv = secretsProvider.getIv(); + byte[] key = huaweiCrypto.encryptBondingKey(secretsProvider.getEncryptMethod(), secretsProvider.getSecretKey(), encryptionKey, iv); + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x01) + .put(0x03, (byte) 0x00) + .put(0x05, clientSerial) + .put(0x06, key) + .put(0x07, iv); + + byte[] serialized = new byte[]{(byte) 0x5A, (byte) 0x00, (byte) 0x44, (byte) 0x00, (byte) 0x01, (byte) 0x0E, (byte) 0x01, (byte) 0x00, (byte) 0x03, (byte) 0x01, (byte) 0x00, (byte) 0x05, (byte) 0x06, (byte) 0x54, (byte) 0x56, (byte) 0x64, (byte) 0x54, (byte) 0x4D, (byte) 0x44, (byte) 0x06, (byte) 0x20, (byte) 0x88, (byte) 0x45, (byte) 0xAA, (byte) 0xB5, (byte) 0x9C, (byte) 0x84, (byte) 0x39, (byte) 0xAE, (byte) 0xD8, (byte) 0xE9, (byte) 0x71, (byte) 0x01, (byte) 0x5D, (byte) 0xC8, (byte) 0x34, (byte) 0x05, (byte) 0xC5, (byte) 0x9A, (byte) 0x6B, (byte) 0xDB, (byte) 0x62, (byte) 0x7D, (byte) 0xC8, (byte) 0xC3, (byte) 0xF4, (byte) 0xCC, (byte) 0x30, (byte) 0x74, (byte) 0x21, (byte) 0xD4, (byte) 0x45, (byte) 0x0E, (byte) 0x07, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x72, (byte) 0xFC}; + DeviceConfig.Bond.Request request = new DeviceConfig.Bond.Request( + secretsProvider, + clientSerial, + key, + iv + ); + + Assert.assertEquals(0x01, request.serviceId); + Assert.assertEquals(0x0E, request.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + Assert.assertTrue(request.complete); + List out = request.serialize(); + Assert.assertEquals(1, out.size()); + Assert.assertArrayEquals(serialized, out.get(0)); + } catch (InvalidAlgorithmParameterException | NoSuchPaddingException | IllegalBlockSizeException | NoSuchAlgorithmException | BadPaddingException | InvalidKeyException | HuaweiPacket.CryptoException e) { + e.printStackTrace(); + Assert.fail(); + } + } + + @Test + public void testBondParamsRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + byte[] clientSerial = new byte[] {(byte) 0x54, (byte) 0x56, (byte) 0x64, (byte) 0x54, (byte) 0x4D, (byte) 0x44}; + byte[] mac = new byte[] {(byte) 0x46, (byte) 0x46, (byte) 0x3A, (byte) 0x46, (byte) 0x46, (byte) 0x3A, (byte) 0x46, (byte) 0x46, (byte) 0x3A, (byte) 0x46, (byte) 0x46, (byte) 0x3A, (byte) 0x46, (byte) 0x46, (byte) 0x3A, (byte) 0x43, (byte) 0x43}; + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x01) + .put(0x03, clientSerial) + .put(0x04, (byte) 0x02) + .put(0x05) + .put(0x07, mac) + .put(0x09); + + byte[] serialized = new byte[] {(byte) 0x5A, (byte) 0x00, (byte) 0x27, (byte) 0x00, (byte) 0x01, (byte) 0x0F, (byte) 0x01, (byte) 0x00, (byte) 0x03, (byte) 0x06, (byte) 0x54, (byte) 0x56, (byte) 0x64, (byte) 0x54, (byte) 0x4D, (byte) 0x44, (byte) 0x04, (byte) 0x01, (byte) 0x02, (byte) 0x05, (byte) 0x00, (byte) 0x07, (byte) 0x11, (byte) 0x46, (byte) 0x46, (byte) 0x3A, (byte) 0x46, (byte) 0x46, (byte) 0x3A, (byte) 0x46, (byte) 0x46, (byte) 0x3A, (byte) 0x46, (byte) 0x46, (byte) 0x3A, (byte) 0x46, (byte) 0x46, (byte) 0x3A, (byte) 0x43, (byte) 0x43, (byte) 0x09, (byte) 0x00, (byte) 0xE5, (byte) 0xD8}; + DeviceConfig.BondParams.Request request = new DeviceConfig.BondParams.Request( + secretsProvider, + clientSerial, + mac + ); + Assert.assertEquals(0x01, request.serviceId); + Assert.assertEquals(0x0F, request.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + Assert.assertTrue(request.complete); + List out = request.serialize(); + Assert.assertEquals(1, out.size()); + Assert.assertArrayEquals(serialized, out.get(0)); + } + + @Test + public void testAuthRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + byte[] challenge = new byte[] {(byte) 0x9D, (byte) 0xF6, (byte) 0x52, (byte) 0x69, (byte) 0x06, (byte) 0x7B, (byte) 0xEB, (byte) 0x46, (byte) 0x94, (byte) 0xAD, (byte) 0x35, (byte) 0xE2, (byte) 0x88, (byte) 0xC3, (byte) 0x84, (byte) 0x24, (byte) 0xA2, (byte) 0x55, (byte) 0xD8, (byte) 0x0F, (byte) 0xA7, (byte) 0x68, (byte) 0x21, (byte) 0x9B, (byte) 0xA1, (byte) 0xC3, (byte) 0xDC, (byte) 0x09, (byte) 0x24, (byte) 0x81, (byte) 0x51, (byte) 0x61}; + byte[] nonce = new byte[] {(byte) 0x00, (byte) 0x01, (byte) 0xBF, (byte) 0x1F, (byte) 0xEF, (byte) 0x9F, (byte) 0xF0, (byte) 0xFE, (byte) 0xEF, (byte) 0xEF, (byte) 0x9F, (byte) 0xEF, (byte) 0xF0, (byte) 0xEF, (byte) 0xF8, (byte) 0xFA, (byte) 0xEF, (byte) 0xF0}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x01, challenge) + .put(0x02, nonce); + + byte[] serialized = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x39, (byte) 0x00, (byte) 0x01, (byte) 0x13, (byte) 0x01, (byte) 0x20, (byte) 0x9d, (byte) 0xf6, (byte) 0x52, (byte) 0x69, (byte) 0x06, (byte) 0x7b, (byte) 0xeb, (byte) 0x46, (byte) 0x94, (byte) 0xad, (byte) 0x35, (byte) 0xe2, (byte) 0x88, (byte) 0xc3, (byte) 0x84, (byte) 0x24, (byte) 0xa2, (byte) 0x55, (byte) 0xd8, (byte) 0x0f, (byte) 0xa7, (byte) 0x68, (byte) 0x21, (byte) 0x9b, (byte) 0xa1, (byte) 0xc3, (byte) 0xdc, (byte) 0x09, (byte) 0x24, (byte) 0x81, (byte) 0x51, (byte) 0x61, (byte) 0x02, (byte) 0x12, (byte) 0x00, (byte) 0x01, (byte) 0xbf, (byte) 0x1f, (byte) 0xef, (byte) 0x9f, (byte) 0xf0, (byte) 0xfe, (byte) 0xef, (byte) 0xef, (byte) 0x9f, (byte) 0xef, (byte) 0xf0, (byte) 0xef, (byte) 0xf8, (byte) 0xfa, (byte) 0xef, (byte) 0xf0, (byte) 0xdc, (byte) 0x88}; + DeviceConfig.Auth.Request request = new DeviceConfig.Auth.Request( + secretsProvider, + challenge, + nonce + ); + + Assert.assertEquals(0x01, request.serviceId); + Assert.assertEquals(0x13, request.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + Assert.assertTrue(request.complete); + List out = request.serialize(); + Assert.assertEquals(1, out.size()); + Assert.assertArrayEquals(serialized, out.get(0)); + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestDisconnectNotification.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestDisconnectNotification.java new file mode 100644 index 000000000..9db856237 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestDisconnectNotification.java @@ -0,0 +1,104 @@ +/* Copyright (C) 2022 Martin.JM + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets; + +import org.junit.Assert; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV; + +public class TestDisconnectNotification { + + HuaweiPacket.ParamsProvider secretsProvider = new HuaweiPacket.ParamsProvider() { + @Override + public byte getDeviceSupportType() { + return 0; + } + + @Override + public byte[] getSecretKey() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public byte[] getIv() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public boolean areTransactionsCrypted() { + return true; + } + + @Override + public int getMtu() { + return 0; + } + + @Override + public int getSliceSize() { + return 0xF4; + } + }; + + @Test + public void testBluetoothDisconnectNotificationSetting() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlvEnable = new HuaweiTLV() + .put(0x01, true); + + byte[] serializedEnable = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x0b, (byte) 0x03, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0xcd, (byte) 0x97, (byte) 0x7e, (byte) 0x01, (byte) 0x48, (byte) 0x34, (byte) 0x2a, (byte) 0x48, (byte) 0x58, (byte) 0x0d, (byte) 0x30, (byte) 0xc7, (byte) 0xbc, (byte) 0x2e, (byte) 0x40, (byte) 0xd4, (byte) 0x20, (byte) 0xaf}; + + DisconnectNotification.DisconnectNotificationSetting.Request requestEnable = new DisconnectNotification.DisconnectNotificationSetting.Request( + secretsProvider, + true + ); + + Assert.assertEquals(0x0b, requestEnable.serviceId); + Assert.assertEquals(0x03, requestEnable.commandId); + Assert.assertEquals(expectedTlvEnable, tlvField.get(requestEnable)); + Assert.assertTrue(requestEnable.complete); + List outEnable = requestEnable.serialize(); + Assert.assertEquals(1, outEnable.size()); + Assert.assertArrayEquals(serializedEnable, outEnable.get(0)); + + + HuaweiTLV expectedTlvDisable = new HuaweiTLV() + .put(0x01, false); + + byte[] serializedDisable = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x0b, (byte) 0x03, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0x28, (byte) 0x00, (byte) 0x99, (byte) 0x6f, (byte) 0x2a, (byte) 0xcb, (byte) 0x62, (byte) 0x3a, (byte) 0xe6, (byte) 0x54, (byte) 0x28, (byte) 0x54, (byte) 0xf8, (byte) 0xab, (byte) 0x54, (byte) 0x83, (byte) 0x39, (byte) 0x9D}; + + DisconnectNotification.DisconnectNotificationSetting.Request requestDisable = new DisconnectNotification.DisconnectNotificationSetting.Request( + secretsProvider, + false + ); + + Assert.assertEquals(0x0b, requestDisable.serviceId); + Assert.assertEquals(0x03, requestDisable.commandId); + Assert.assertEquals(expectedTlvDisable, tlvField.get(requestDisable)); + Assert.assertTrue(requestDisable.complete); + List outDisable = requestDisable.serialize(); + Assert.assertEquals(1, outDisable.size()); + Assert.assertArrayEquals(serializedDisable, outDisable.get(0)); + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestFindPhone.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestFindPhone.java new file mode 100644 index 000000000..7204f200f --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestFindPhone.java @@ -0,0 +1,120 @@ +/* Copyright (C) 2022 Martin.JM + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets; + +import org.junit.Assert; +import org.junit.Test; + +import java.lang.reflect.Field; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV; + +public class TestFindPhone { + + HuaweiPacket.ParamsProvider secretsProvider = new HuaweiPacket.ParamsProvider() { + @Override + public byte getDeviceSupportType() { + return 0; + } + + @Override + public byte[] getSecretKey() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public byte[] getIv() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public boolean areTransactionsCrypted() { + return true; + } + + @Override + public int getMtu() { + return 0; + } + + @Override + public int getSliceSize() { + return 0xF4; + } + }; + + @Test + public void testStartFindPhone() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] raw = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x06, (byte) 0x00, (byte) 0x0b, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0xcc, (byte) 0xf1}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x01, true); + + HuaweiPacket packet = new HuaweiPacket(secretsProvider).parse(raw); + packet.parseTlv(); + + Assert.assertEquals(0x0b, packet.serviceId); + Assert.assertEquals(0x01, packet.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packet)); + Assert.assertTrue(packet.complete); + Assert.assertTrue(packet instanceof FindPhone.Response); + Assert.assertTrue(((FindPhone.Response) packet).start); + } + + @Test + public void testStopFindPhone() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] raw = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x06, (byte) 0x00, (byte) 0x0b, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0xdc, (byte) 0xd0}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x01, false); + + HuaweiPacket packet = new HuaweiPacket(secretsProvider).parse(raw); + packet.parseTlv(); + + Assert.assertEquals(0x0b, packet.serviceId); + Assert.assertEquals(0x01, packet.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packet)); + Assert.assertTrue(packet.complete); + Assert.assertTrue(packet instanceof FindPhone.Response); + Assert.assertFalse(((FindPhone.Response) packet).start); + } + + @Test + public void testFindPhoneMissingTag() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] raw = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x03, (byte) 0x00, (byte) 0x0b, (byte) 0x01, (byte) 0xa1, (byte) 0x91}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV(); + + HuaweiPacket packet = new HuaweiPacket(secretsProvider).parse(raw); + packet.parseTlv(); + + Assert.assertEquals(0x0b, packet.serviceId); + Assert.assertEquals(0x01, packet.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packet)); + Assert.assertTrue(packet.complete); + Assert.assertTrue(packet instanceof FindPhone.Response); + Assert.assertFalse(((FindPhone.Response) packet).start); + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestFitnessData.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestFitnessData.java new file mode 100644 index 000000000..717944dc3 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestFitnessData.java @@ -0,0 +1,502 @@ +/* Copyright (C) 2022 Martin.JM + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets; + +import org.junit.Assert; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCrypto; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV; + +public class TestFitnessData { + + HuaweiPacket.ParamsProvider secretsProvider = new HuaweiPacket.ParamsProvider() { + @Override + public byte getDeviceSupportType() { + return 0; + } + + @Override + public byte[] getSecretKey() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public byte[] getIv() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public boolean areTransactionsCrypted() { + return true; + } + + @Override + public int getMtu() { + return 0; + } + + @Override + public int getSliceSize() { + return 0xF4; + } + }; + + @Test + public void testMessageCountRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + int startSleep = 0x00000000; + int endSleep = 0x01020304; + int startStep = 0x01020304; + int endStep = 0x10203040; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlvSleep = new HuaweiTLV() + .put(0x81) + .put(0x03, startSleep) + .put(0x04, endSleep); + HuaweiTLV expectedTlvStep = new HuaweiTLV() + .put(0x81) + .put(0x03, startStep) + .put(0x04, endStep); + + byte[] sleepSerialized = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x07, (byte) 0x0c, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0x32, (byte) 0x69, (byte) 0x7b, (byte) 0x51, (byte) 0x85, (byte) 0x20, (byte) 0x9b, (byte) 0x16, (byte) 0x6b, (byte) 0x93, (byte) 0x8a, (byte) 0x3d, (byte) 0xd5, (byte) 0x9a, (byte) 0xf9, (byte) 0x29, (byte) 0xdf, (byte) 0x07}; + byte[] stepSerialized = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x07, (byte) 0x0a, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0x4d, (byte) 0x52, (byte) 0x79, (byte) 0x57, (byte) 0x49, (byte) 0x30, (byte) 0x75, (byte) 0xc6, (byte) 0x28, (byte) 0x5b, (byte) 0x79, (byte) 0xd5, (byte) 0xab, (byte) 0x89, (byte) 0x0d, (byte) 0x1e, (byte) 0xa9, (byte) 0xc9}; + + FitnessData.MessageCount.Request sleepRequest = new FitnessData.MessageCount.Request(secretsProvider, FitnessData.MessageCount.sleepId, startSleep, endSleep); + FitnessData.MessageCount.Request stepRequest = new FitnessData.MessageCount.Request(secretsProvider, FitnessData.MessageCount.stepId, startStep, endStep); + + Assert.assertEquals(0x07, sleepRequest.serviceId); + Assert.assertEquals(0x0c, sleepRequest.commandId); + Assert.assertEquals(expectedTlvSleep, tlvField.get(sleepRequest)); + Assert.assertTrue(sleepRequest.complete); + List outSleep = sleepRequest.serialize(); + Assert.assertEquals(1, outSleep.size()); + Assert.assertArrayEquals(sleepSerialized, outSleep.get(0)); + + Assert.assertEquals(0x07, stepRequest.serviceId); + Assert.assertEquals(0x0a, stepRequest.commandId); + Assert.assertEquals(expectedTlvStep, tlvField.get(stepRequest)); + Assert.assertTrue(stepRequest.complete); + List outStep = stepRequest.serialize(); + Assert.assertEquals(1, outStep.size()); + Assert.assertArrayEquals(stepSerialized, outStep.get(0)); + } + + @Test + public void testMessageCountResponse() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] rawSleep = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x07, (byte) 0x0c, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0xf6, (byte) 0xfb, (byte) 0xc0, (byte) 0xb6, (byte) 0x4f, (byte) 0x9a, (byte) 0xfa, (byte) 0x77, (byte) 0x53, (byte) 0x28, (byte) 0x7d, (byte) 0x13, (byte) 0xca, (byte) 0x49, (byte) 0xda, (byte) 0xfd, (byte) 0x93, (byte) 0x09}; + byte[] rawStep = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x07, (byte) 0x0a, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0xf6, (byte) 0xfb, (byte) 0xc0, (byte) 0xb6, (byte) 0x4f, (byte) 0x9a, (byte) 0xfa, (byte) 0x77, (byte) 0x53, (byte) 0x28, (byte) 0x7d, (byte) 0x13, (byte) 0xca, (byte) 0x49, (byte) 0xda, (byte) 0xfd, (byte) 0xd4, (byte) 0x93}; + + short expectedCount = 0x1337; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x81, new HuaweiTLV().put(0x02, expectedCount)); + + HuaweiPacket packetSleep = new HuaweiPacket(secretsProvider).parse(rawSleep); + HuaweiPacket packetStep = new HuaweiPacket(secretsProvider).parse(rawStep); + packetSleep.parseTlv(); + packetStep.parseTlv(); + + Assert.assertEquals(0x07, packetSleep.serviceId); + Assert.assertEquals(0x0c, packetSleep.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packetSleep)); + Assert.assertTrue(packetSleep.complete); + Assert.assertTrue(packetSleep instanceof FitnessData.MessageCount.Response); + Assert.assertEquals(expectedCount, ((FitnessData.MessageCount.Response) packetSleep).count); + + Assert.assertEquals(0x07, packetStep.serviceId); + Assert.assertEquals(0x0a, packetStep.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packetStep)); + Assert.assertTrue(packetStep.complete); + Assert.assertTrue(packetStep instanceof FitnessData.MessageCount.Response); + Assert.assertEquals(expectedCount, ((FitnessData.MessageCount.Response) packetStep).count); + } + + @Test + public void testMessageDataRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiCrypto.CryptoException, HuaweiPacket.CryptoException { + short count = 0x1337; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x81, new HuaweiTLV().put(0x02, count)); + expectedTlv.encrypt(secretsProvider); + + byte[] expectedSleep = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x07, (byte) 0x0d, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0xf6, (byte) 0xfb, (byte) 0xc0, (byte) 0xb6, (byte) 0x4f, (byte) 0x9a, (byte) 0xfa, (byte) 0x77, (byte) 0x53, (byte) 0x28, (byte) 0x7d, (byte) 0x13, (byte) 0xca, (byte) 0x49, (byte) 0xda, (byte) 0xfd, (byte) 0x7d, (byte) 0xad}; + byte[] expectedStep = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x07, (byte) 0x0b, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0xf6, (byte) 0xfb, (byte) 0xc0, (byte) 0xb6, (byte) 0x4f, (byte) 0x9a, (byte) 0xfa, (byte) 0x77, (byte) 0x53, (byte) 0x28, (byte) 0x7d, (byte) 0x13, (byte) 0xca, (byte) 0x49, (byte) 0xda, (byte) 0xfd, (byte) 0x3a, (byte) 0x37}; + + FitnessData.MessageData.Request sleepRequest = new FitnessData.MessageData.Request(secretsProvider, FitnessData.MessageData.sleepId, count); + FitnessData.MessageData.Request stepRequest = new FitnessData.MessageData.Request(secretsProvider, FitnessData.MessageData.stepId, count); + + Assert.assertEquals(0x07, sleepRequest.serviceId); + Assert.assertEquals(0x0d, sleepRequest.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(sleepRequest)); + Assert.assertTrue(sleepRequest.complete); + List outSleep = sleepRequest.serialize(); + Assert.assertEquals(1, outSleep.size()); + Assert.assertArrayEquals(expectedSleep, outSleep.get(0)); + + Assert.assertEquals(0x07, stepRequest.serviceId); + Assert.assertEquals(0x0b, stepRequest.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(stepRequest)); + Assert.assertTrue(stepRequest.complete); + List outStep = stepRequest.serialize(); + Assert.assertEquals(1, outStep.size()); + Assert.assertArrayEquals(expectedStep, outStep.get(0)); + } + + @Test + public void testMessageDataSleepResponse() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] raw = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x3a, (byte) 0x00, (byte) 0x07, (byte) 0x0d, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x20, (byte) 0xa4, (byte) 0x9e, (byte) 0xd8, (byte) 0xd3, (byte) 0x7a, (byte) 0x0e, (byte) 0x51, (byte) 0x55, (byte) 0xc5, (byte) 0x48, (byte) 0x07, (byte) 0x99, (byte) 0xf5, (byte) 0x99, (byte) 0x48, (byte) 0x3e, (byte) 0x41, (byte) 0xed, (byte) 0x16, (byte) 0xf1, (byte) 0x52, (byte) 0xd2, (byte) 0x9f, (byte) 0x38, (byte) 0xe8, (byte) 0xb1, (byte) 0x83, (byte) 0xd6, (byte) 0xcb, (byte) 0x52, (byte) 0xb0, (byte) 0x9f, (byte) 0x48, (byte) 0x05}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x81, new HuaweiTLV() + .put(0x02, (short) 0x1337) + .put(0x83, new HuaweiTLV() + .put(0x04, (byte) 0x00) + .put(0x05, new byte[] {}) + ) + .put(0x83, new HuaweiTLV() + .put(0x04, (byte) 0x01) + .put(0x05, new byte[] {0x01, 0x02}) + ) + ); + + HuaweiPacket packet = new HuaweiPacket(secretsProvider).parse(raw); + packet.parseTlv(); + + Assert.assertEquals(0x07, packet.serviceId); + Assert.assertEquals(0x0d, packet.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packet)); + Assert.assertTrue(packet.complete); + Assert.assertTrue(packet instanceof FitnessData.MessageData.SleepResponse); + Assert.assertEquals(0x1337, ((FitnessData.MessageData.SleepResponse) packet).number); + Assert.assertEquals(2, ((FitnessData.MessageData.SleepResponse) packet).containers.size()); + Assert.assertEquals(0x00, ((FitnessData.MessageData.SleepResponse) packet).containers.get(0).type); + Assert.assertArrayEquals(new byte[] {}, ((FitnessData.MessageData.SleepResponse) packet).containers.get(0).timestamp); + Assert.assertEquals(0x01, ((FitnessData.MessageData.SleepResponse) packet).containers.get(1).type); + Assert.assertArrayEquals(new byte[] {0x01, 0x02}, ((FitnessData.MessageData.SleepResponse) packet).containers.get(1).timestamp); + } + + @Test + public void testMessageDataStepResponse() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] raw = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x5a, (byte) 0x00, (byte) 0x07, (byte) 0x0b, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x40, (byte) 0xdc, (byte) 0xb7, (byte) 0xf6, (byte) 0xaa, (byte) 0xb2, (byte) 0xf1, (byte) 0x03, (byte) 0x53, (byte) 0x25, (byte) 0x39, (byte) 0xe4, (byte) 0x79, (byte) 0xdd, (byte) 0xbf, (byte) 0x18, (byte) 0x7b, (byte) 0x98, (byte) 0x30, (byte) 0xb7, (byte) 0x4c, (byte) 0x33, (byte) 0xd2, (byte) 0x0c, (byte) 0xa5, (byte) 0xee, (byte) 0xfe, (byte) 0x5f, (byte) 0xa5, (byte) 0x12, (byte) 0x20, (byte) 0xec, (byte) 0x79, (byte) 0x38, (byte) 0xec, (byte) 0x9e, (byte) 0x4d, (byte) 0xfc, (byte) 0xc3, (byte) 0x5c, (byte) 0x59, (byte) 0x67, (byte) 0x51, (byte) 0x4b, (byte) 0xef, (byte) 0x50, (byte) 0x48, (byte) 0xb7, (byte) 0xf8, (byte) 0xc7, (byte) 0xe3, (byte) 0xf7, (byte) 0xdf, (byte) 0x82, (byte) 0xb4, (byte) 0x1a, (byte) 0xb8, (byte) 0x94, (byte) 0x78, (byte) 0x0d, (byte) 0xda, (byte) 0x53, (byte) 0xe3, (byte) 0xbe, (byte) 0xbf, (byte) 0x21, (byte) 0xc2}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + // TODO: add HR data + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x81, new HuaweiTLV() + .put(0x02, (short) 0x1337) + .put(0x03, 0xCAFEBEEF) + .put(0x084, new HuaweiTLV() + .put(0x05, (byte) 0x00) + .put(0x06, new byte[] {}) + ) + .put(0x84, new HuaweiTLV() + .put(0x05, (byte) 0x01) + .put(0x06, new byte[] {0x01, 0x02}) + ) + .put(0x84, new HuaweiTLV() + .put(0x05, (byte) 0x02) + .put(0x06, new byte[] {0x0e, 0x00, 0x01, 0x00, 0x02, 0x00, 0x03}) + ) + .put(0x84, new HuaweiTLV() + .put(0x05, (byte) 0x02) + .put(0x06, new byte[] {0x01, 0x00, 0x01}) + ) + ); + + HuaweiPacket packet = new HuaweiPacket(secretsProvider).parse(raw); + packet.parseTlv(); + + Assert.assertEquals(0x07, packet.serviceId); + Assert.assertEquals(0x0b, packet.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packet)); + Assert.assertTrue(packet.complete); + Assert.assertTrue(packet instanceof FitnessData.MessageData.StepResponse); + + Assert.assertEquals(0x1337, ((FitnessData.MessageData.StepResponse) packet).number); + Assert.assertEquals(0xCAFEBEEF, ((FitnessData.MessageData.StepResponse) packet).timestamp); + Assert.assertEquals(4, ((FitnessData.MessageData.StepResponse) packet).containers.size()); + + Assert.assertEquals(0x00, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).timestampOffset); + Assert.assertArrayEquals(new byte[] {}, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).data); + Assert.assertEquals(0xCAFEBEEF, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).timestamp); + Assert.assertNull(((FitnessData.MessageData.StepResponse) packet).containers.get(0).parsedData); + Assert.assertEquals("Data is missing feature bitmap.", ((FitnessData.MessageData.StepResponse) packet).containers.get(0).parsedDataError); + Assert.assertEquals(-1, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).steps); + Assert.assertEquals(-1, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).calories); + Assert.assertEquals(-1, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).distance); + Assert.assertNull(((FitnessData.MessageData.StepResponse) packet).containers.get(0).unknownTVs); + + Assert.assertEquals(0x01, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).timestampOffset); + Assert.assertArrayEquals(new byte[] {0x01, 0x02}, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).data); + Assert.assertEquals(0xCAFEBF2B, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).timestamp); + Assert.assertNull(((FitnessData.MessageData.StepResponse) packet).containers.get(1).parsedData); + Assert.assertEquals("Data is too short for selected features.", ((FitnessData.MessageData.StepResponse) packet).containers.get(1).parsedDataError); + Assert.assertEquals(-1, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).steps); + Assert.assertEquals(-1, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).calories); + Assert.assertEquals(-1, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).distance); + Assert.assertEquals(0, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).unknownTVs.size()); + + Assert.assertEquals(0x02, ((FitnessData.MessageData.StepResponse) packet).containers.get(2).timestampOffset); + Assert.assertArrayEquals(new byte[] {0x0e, 0x00, 0x01, 0x00, 0x02, 0x00, 0x03}, ((FitnessData.MessageData.StepResponse) packet).containers.get(2).data); + Assert.assertEquals(0xCAFEBF67, ((FitnessData.MessageData.StepResponse) packet).containers.get(2).timestamp); + Assert.assertEquals(3, ((FitnessData.MessageData.StepResponse) packet).containers.get(2).parsedData.size()); + Assert.assertEquals(1, ((FitnessData.MessageData.StepResponse) packet).containers.get(2).parsedData.get(0).bitmap); + Assert.assertEquals(0x02, ((FitnessData.MessageData.StepResponse) packet).containers.get(2).parsedData.get(0).tag); + Assert.assertEquals(0x01, ((FitnessData.MessageData.StepResponse) packet).containers.get(2).parsedData.get(0).value); + Assert.assertEquals(1, ((FitnessData.MessageData.StepResponse) packet).containers.get(2).parsedData.get(1).bitmap); + Assert.assertEquals(0x04, ((FitnessData.MessageData.StepResponse) packet).containers.get(2).parsedData.get(1).tag); + Assert.assertEquals(0x02, ((FitnessData.MessageData.StepResponse) packet).containers.get(2).parsedData.get(1).value); + Assert.assertEquals(1, ((FitnessData.MessageData.StepResponse) packet).containers.get(2).parsedData.get(2).bitmap); + Assert.assertEquals(0x08, ((FitnessData.MessageData.StepResponse) packet).containers.get(2).parsedData.get(2).tag); + Assert.assertEquals(0x03, ((FitnessData.MessageData.StepResponse) packet).containers.get(2).parsedData.get(2).value); + Assert.assertEquals("", ((FitnessData.MessageData.StepResponse) packet).containers.get(2).parsedDataError); + Assert.assertEquals(0x01, ((FitnessData.MessageData.StepResponse) packet).containers.get(2).steps); + Assert.assertEquals(0x02, ((FitnessData.MessageData.StepResponse) packet).containers.get(2).calories); + Assert.assertEquals(0x03, ((FitnessData.MessageData.StepResponse) packet).containers.get(2).distance); + Assert.assertEquals(0, ((FitnessData.MessageData.StepResponse) packet).containers.get(2).unknownTVs.size()); + + Assert.assertEquals(0x02, ((FitnessData.MessageData.StepResponse) packet).containers.get(3).timestampOffset); + Assert.assertArrayEquals(new byte[] {0x01, 0x00, 0x01}, ((FitnessData.MessageData.StepResponse) packet).containers.get(3).data); + Assert.assertEquals(0xCAFEBF67, ((FitnessData.MessageData.StepResponse) packet).containers.get(3).timestamp); + Assert.assertEquals(1, ((FitnessData.MessageData.StepResponse) packet).containers.get(3).parsedData.size()); + Assert.assertEquals(1, ((FitnessData.MessageData.StepResponse) packet).containers.get(3).parsedData.get(0).bitmap); + Assert.assertEquals(0x01, ((FitnessData.MessageData.StepResponse) packet).containers.get(3).parsedData.get(0).tag); + Assert.assertEquals(0x01, ((FitnessData.MessageData.StepResponse) packet).containers.get(3).parsedData.get(0).value); + Assert.assertEquals("", ((FitnessData.MessageData.StepResponse) packet).containers.get(3).parsedDataError); + Assert.assertEquals(-1, ((FitnessData.MessageData.StepResponse) packet).containers.get(3).steps); + Assert.assertEquals(-1, ((FitnessData.MessageData.StepResponse) packet).containers.get(3).calories); + Assert.assertEquals(-1, ((FitnessData.MessageData.StepResponse) packet).containers.get(3).distance); + Assert.assertEquals(1, ((FitnessData.MessageData.StepResponse) packet).containers.get(3).unknownTVs.size()); + Assert.assertEquals(1, ((FitnessData.MessageData.StepResponse) packet).containers.get(3).unknownTVs.get(0).bitmap); + Assert.assertEquals(0x01, ((FitnessData.MessageData.StepResponse) packet).containers.get(3).unknownTVs.get(0).tag); + Assert.assertEquals(0x01, ((FitnessData.MessageData.StepResponse) packet).containers.get(3).unknownTVs.get(0).value); + } + + @Test + public void testMessageDataStepResponseSingleByte() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] raw = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x3a, (byte) 0x00, (byte) 0x07, (byte) 0x0b, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x20, (byte) 0xcf, (byte) 0xa7, (byte) 0x76, (byte) 0x30, (byte) 0x69, (byte) 0xa3, (byte) 0x83, (byte) 0x6e, (byte) 0xd2, (byte) 0x84, (byte) 0x70, (byte) 0xc8, (byte) 0xca, (byte) 0x94, (byte) 0x87, (byte) 0xd2, (byte) 0x0d, (byte) 0x1e, (byte) 0xf5, (byte) 0x60, (byte) 0x72, (byte) 0xa4, (byte) 0xd9, (byte) 0x8f, (byte) 0xf6, (byte) 0xdf, (byte) 0x09, (byte) 0x35, (byte) 0x3c, (byte) 0x86, (byte) 0x62, (byte) 0x00, (byte) 0x0a, (byte) 0x3b}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + // TODO: change test as 0x40 is now added as HR + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x81, new HuaweiTLV() + .put(0x02, (short) 0x01) + .put(0x03, 0x02) + .put(0x84, new HuaweiTLV() + .put(0x05, (byte) 0x00) + .put(0x06, new byte[] {0x20, 0x01}) + ) + .put(0x84, new HuaweiTLV() + .put(0x05, (byte) 0x01) + .put(0x06, new byte[] {0x40, 0x02}) + ) + ); + + HuaweiPacket packet = new HuaweiPacket(secretsProvider).parse(raw); + packet.parseTlv(); + + Assert.assertEquals(0x07, packet.serviceId); + Assert.assertEquals(0x0b, packet.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packet)); + Assert.assertTrue(packet.complete); + Assert.assertTrue(packet instanceof FitnessData.MessageData.StepResponse); + + Assert.assertEquals(0x01, ((FitnessData.MessageData.StepResponse) packet).number); + Assert.assertEquals(0x02, ((FitnessData.MessageData.StepResponse) packet).timestamp); + Assert.assertEquals(2, ((FitnessData.MessageData.StepResponse) packet).containers.size()); + + Assert.assertEquals(0x00, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).timestampOffset); + Assert.assertArrayEquals(new byte[] {0x20, 0x01}, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).data); + Assert.assertEquals(0x02, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).timestamp); + Assert.assertEquals(1, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).parsedData.size()); + Assert.assertEquals(1, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).parsedData.get(0).bitmap); + Assert.assertEquals(0x20, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).parsedData.get(0).tag); + Assert.assertEquals(0x01, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).parsedData.get(0).value); + Assert.assertEquals("", ((FitnessData.MessageData.StepResponse) packet).containers.get(0).parsedDataError); + Assert.assertEquals(-1, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).steps); + Assert.assertEquals(-1, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).calories); + Assert.assertEquals(-1, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).distance); + Assert.assertEquals(1, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).unknownTVs.size()); + Assert.assertEquals(1, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).unknownTVs.get(0).bitmap); + Assert.assertEquals(0x20, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).unknownTVs.get(0).tag); + Assert.assertEquals(0x01, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).unknownTVs.get(0).value); + + Assert.assertEquals(0x01, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).timestampOffset); + Assert.assertArrayEquals(new byte[] {0x40, 0x02}, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).data); + Assert.assertEquals(0x3e, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).timestamp); + Assert.assertEquals(1, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).parsedData.size()); + Assert.assertEquals(1, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).parsedData.get(0).bitmap); + Assert.assertEquals(0x40, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).parsedData.get(0).tag); + Assert.assertEquals(0x02, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).parsedData.get(0).value); + Assert.assertEquals("", ((FitnessData.MessageData.StepResponse) packet).containers.get(1).parsedDataError); + Assert.assertEquals(-1, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).steps); + Assert.assertEquals(-1, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).calories); + Assert.assertEquals(-1, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).distance); + Assert.assertEquals(0, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).unknownTVs.size()); +// Assert.assertEquals(1, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).unknownTVs.get(0).bitmap); +// Assert.assertEquals(0x40, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).unknownTVs.get(0).tag); +// Assert.assertEquals(0x02, ((FitnessData.MessageData.StepResponse) packet).containers.get(1).unknownTVs.get(0).value); + } + + @SuppressWarnings("ConstantConditions") + @Test + public void testActivityReminderRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException, HuaweiCrypto.CryptoException { + boolean longSitSwitch = false; + byte longSitInterval = 0x00; + byte[] longSitStart = {0x01, 0x02}; + byte[] longSitEnd = {0x03, 0x04}; + byte cycle = 0x05; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x81, new HuaweiTLV() + .put(0x02, longSitSwitch) + .put(0x03, longSitInterval) + .put(0x04, longSitStart) + .put(0x05, longSitEnd) + .put(0x06, cycle) + ); + expectedTlv.encrypt(secretsProvider); + + byte[] expected = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x3a, (byte) 0x00, (byte) 0x07, (byte) 0x07, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x20, (byte) 0x5b, (byte) 0x9b, (byte) 0x16, (byte) 0xa8, (byte) 0x65, (byte) 0x81, (byte) 0xc1, (byte) 0x18, (byte) 0x2f, (byte) 0x42, (byte) 0xab, (byte) 0xf3, (byte) 0x43, (byte) 0x1e, (byte) 0x5c, (byte) 0x32, (byte) 0x9a, (byte) 0xa9, (byte) 0xa2, (byte) 0x18, (byte) 0x36, (byte) 0xb3, (byte) 0x60, (byte) 0x39, (byte) 0xeb, (byte) 0xdb, (byte) 0x6b, (byte) 0xe5, (byte) 0xac, (byte) 0x7b, (byte) 0x45, (byte) 0x36, (byte) 0xbc, (byte) 0x0c}; + + FitnessData.ActivityReminder.Request request = new FitnessData.ActivityReminder.Request( + secretsProvider, + longSitSwitch, + longSitInterval, + longSitStart, + longSitEnd, + cycle + ); + + Assert.assertEquals(0x07, request.serviceId); + Assert.assertEquals(0x07, request.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + Assert.assertTrue(request.complete); + List out = request.serialize(); + Assert.assertEquals(1, out.size()); + Assert.assertArrayEquals(expected, out.get(0)); + } + + @SuppressWarnings("ConstantConditions") + @Test + public void testTruSleepRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiCrypto.CryptoException, HuaweiPacket.CryptoException { + boolean truSleepSwitch = false; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x01, truSleepSwitch); + expectedTlv.encrypt(secretsProvider); + + byte [] expected = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x07, (byte) 0x16, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0x28, (byte) 0x00, (byte) 0x99, (byte) 0x6f, (byte) 0x2a, (byte) 0xcb, (byte) 0x62, (byte) 0x3a, (byte) 0xe6, (byte) 0x54, (byte) 0x28, (byte) 0x54, (byte) 0xf8, (byte) 0xab, (byte) 0x54, (byte) 0x83, (byte) 0x02, (byte) 0x23}; + + FitnessData.TruSleep.Request request = new FitnessData.TruSleep.Request(secretsProvider, truSleepSwitch); + + Assert.assertEquals(0x07, request.serviceId); + Assert.assertEquals(0x16, request.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + Assert.assertTrue(request.complete); + List out = request.serialize(); + Assert.assertEquals(1, out.size()); + Assert.assertArrayEquals(expected, out.get(0)); + } + + @Test + public void testMessageDataStepResponseNoCount() throws NoSuchFieldException, IllegalAccessException { + // I've seen this happening because of a bug in the counts, so it's probably best to stop the sync if this happens. + byte[] raw = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x07, (byte) 0x0b, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0x4c, (byte) 0xdd, (byte) 0x99, (byte) 0x79, (byte) 0xf6, (byte) 0x3c, (byte) 0x1e, (byte) 0xbb, (byte) 0x0a, (byte) 0x95, (byte) 0x8d, (byte) 0x12, (byte) 0x05, (byte) 0x81, (byte) 0x7f, (byte) 0xff, (byte) 0xeb, (byte) 0x45}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + try { + HuaweiPacket packet = new HuaweiPacket(secretsProvider).parse(raw); + packet.parseTlv(); + Assert.fail(); + } catch (HuaweiPacket.ParseException e) { + if (e instanceof HuaweiPacket.MissingTagException) { + Assert.assertNotNull(e.getMessage()); + if (!e.getMessage().equals("Missing tag: 2")) { + Assert.fail(); + } + } else { + Assert.fail(); + } + } + } + + @Test + public void testSpoData() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] raw = {(byte) 0x5a, (byte) 0x00, (byte) 0x3a, (byte) 0x00, (byte) 0x07, (byte) 0x0b, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x20, (byte) 0x30, (byte) 0xdf, (byte) 0x42, (byte) 0xc9, (byte) 0x79, (byte) 0x91, (byte) 0x36, (byte) 0x3d, (byte) 0x80, (byte) 0x6b, (byte) 0x99, (byte) 0xd3, (byte) 0x3f, (byte) 0xbf, (byte) 0x1f, (byte) 0x1e, (byte) 0xc1, (byte) 0x0b, (byte) 0xbf, (byte) 0xcd, (byte) 0xae, (byte) 0x38, (byte) 0x89, (byte) 0x60, (byte) 0x60, (byte) 0xf7, (byte) 0x93, (byte) 0x84, (byte) 0x3a, (byte) 0x09, (byte) 0xd3, (byte) 0x77, (byte) 0x1e, (byte) 0xb9}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x81, new HuaweiTLV() + .put(0x02, (short) 0x1337) + .put(0x03, 0xCAFEBEEF) + .put(0x84, new HuaweiTLV() + .put(0x05, (byte) 0x00) + .put(0x06, new byte[] {(byte) 0x80, 0x01, 0x42}) + ) + ); + + HuaweiPacket packet = new HuaweiPacket(secretsProvider).parse(raw); + packet.parseTlv(); + + Assert.assertEquals(0x07, packet.serviceId); + Assert.assertEquals(0x0b, packet.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packet)); + Assert.assertTrue(packet.complete); + Assert.assertTrue(packet instanceof FitnessData.MessageData.StepResponse); + + Assert.assertEquals(0x1337, ((FitnessData.MessageData.StepResponse) packet).number); + Assert.assertEquals(0xCAFEBEEF, ((FitnessData.MessageData.StepResponse) packet).timestamp); + Assert.assertEquals(1, ((FitnessData.MessageData.StepResponse) packet).containers.size()); + + Assert.assertEquals(0x00, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).timestampOffset); + Assert.assertArrayEquals(new byte[] {(byte) 0x80, 0x01, 0x42}, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).data); + Assert.assertEquals(0xCAFEBEEF, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).timestamp); + Assert.assertEquals(1, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).parsedData.size()); + Assert.assertEquals(2, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).parsedData.get(0).bitmap); + Assert.assertEquals(0x01, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).parsedData.get(0).tag); + Assert.assertEquals(0x42, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).parsedData.get(0).value); + Assert.assertEquals("", ((FitnessData.MessageData.StepResponse) packet).containers.get(0).parsedDataError); + Assert.assertEquals(0, ((FitnessData.MessageData.StepResponse) packet).containers.get(0).unknownTVs.size()); + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestLocaleConfig.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestLocaleConfig.java new file mode 100644 index 000000000..a8b288cc3 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestLocaleConfig.java @@ -0,0 +1,87 @@ +/* Copyright (C) 2022 Martin.JM + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets; + +import org.junit.Assert; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV; + +public class TestLocaleConfig { + + HuaweiPacket.ParamsProvider paramsProvider = new HuaweiPacket.ParamsProvider() { + @Override + public byte getDeviceSupportType() { + return 0; + } + + @Override + public byte[] getSecretKey() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public byte[] getIv() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public boolean areTransactionsCrypted() { + return true; + } + + @Override + public int getMtu() { + return 0; + } + + @Override + public int getSliceSize() { + return 0xF4; + } + }; + + @Test + public void testSetLocaleRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x01, new byte[] {0x45, 0x4e, 0x2d, 0x47, 0x42}) + .put(0x02, (byte) 0x00); + + byte[] serialized = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x0c, (byte) 0x01, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0x4e, (byte) 0xb0, (byte) 0x71, (byte) 0x05, (byte) 0x7b, (byte) 0xf1, (byte) 0x07, (byte) 0x31, (byte) 0xc4, (byte) 0x6c, (byte) 0x5b, (byte) 0x6d, (byte) 0xbf, (byte) 0x07, (byte) 0xf5, (byte) 0x55, (byte) 0x65, (byte) 0x06}; + + LocaleConfig.SetLanguageSetting request = new LocaleConfig.SetLanguageSetting( + paramsProvider, + new byte[] {0x45, 0x4e, 0x2d, 0x47, 0x42}, + (byte) 0x00 + ); + + Assert.assertEquals(0x0c, request.serviceId); + Assert.assertEquals(0x01, request.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + Assert.assertTrue(request.complete); + List out = request.serialize(); + Assert.assertEquals(1, out.size()); + Assert.assertArrayEquals(serialized, out.get(0)); + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestMusicControl.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestMusicControl.java new file mode 100644 index 000000000..e45bf134e --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestMusicControl.java @@ -0,0 +1,338 @@ +/* Copyright (C) 2022-2023 Martin.JM + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets; + +import static nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.MusicControl.Control.Response.Button; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV; + +public class TestMusicControl { + + HuaweiPacket.ParamsProvider secretsProvider = new HuaweiPacket.ParamsProvider() { + @Override + public byte getDeviceSupportType() { + return 0; + } + + @Override + public byte[] getSecretKey() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public byte[] getIv() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public boolean areTransactionsCrypted() { + return true; + } + + @Override + public int getMtu() { + return 0; + } + + @Override + public int getSliceSize() { + return 0xF4; + } + }; + + @Test + public void testMusicStatusRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + int okInput = 0x000186a0; + int errInput = 0x00000000; + + byte commandId1 = 0x01; + byte commandId2 = 0x02; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV okExpectedTlv = new HuaweiTLV() + .put(0x7f, okInput); + HuaweiTLV errExpectedTlv = new HuaweiTLV() + .put(0x7f, errInput); + + MusicControl.MusicStatusRequest okRequest = new MusicControl.MusicStatusRequest(secretsProvider, commandId1, okInput); + MusicControl.MusicStatusRequest errRequest = new MusicControl.MusicStatusRequest(secretsProvider, commandId2, errInput); + + Assert.assertEquals(0x25, okRequest.serviceId); + Assert.assertEquals(commandId1, okRequest.commandId); + Assert.assertEquals(okExpectedTlv, tlvField.get(okRequest)); + Assert.assertTrue(okRequest.complete); + + // To check it doesn't error + okRequest.serialize(); + + Assert.assertEquals(0x25, errRequest.serviceId); + Assert.assertEquals(commandId2, errRequest.commandId); + Assert.assertEquals(errExpectedTlv, tlvField.get(errRequest)); + Assert.assertTrue(errRequest.complete); + + // To check it doesn't error + errRequest.serialize(); + } + + @Test + public void testMusicStatusResponse() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] raw = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x25, (byte) 0x01, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0x01, (byte) 0x43, (byte) 0xdb, (byte) 0x63, (byte) 0xee, (byte) 0x66, (byte) 0xb0, (byte) 0xcd, (byte) 0xff, (byte) 0x9f, (byte) 0x69, (byte) 0x91, (byte) 0x76, (byte) 0x80, (byte) 0x15, (byte) 0x1e, (byte) 0x52, (byte) 0x46}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV(); + + HuaweiPacket packet = new HuaweiPacket(secretsProvider).parse(raw); + packet.parseTlv(); + + Assert.assertEquals(0x25, packet.serviceId); + Assert.assertEquals(0x01, packet.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packet)); + Assert.assertTrue(packet instanceof MusicControl.MusicStatusResponse); + + // TODO: complete test when more is known about packet contents + } + + @Test + public void testMusicStatusResponseUnencrypted() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] rawOk = new byte[] {0x5a, 0x00, 0x09, 0x00, 0x25, 0x01, 0x7f, 0x04, 0x00, 0x01, (byte) 0x86, (byte) 0xa0, 0x63, (byte) 0x96}; + byte[] rawErr = new byte[] {0x5a, 0x00, 0x09, 0x00, 0x25, 0x01, 0x7f, 0x04, 0x00, 0x01, (byte) 0x86, (byte) 0xaa, (byte) 0xc2, (byte) 0xdc}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlvOk = new HuaweiTLV().put(0x7F, 0x000186A0); + HuaweiTLV expectedTlvErr = new HuaweiTLV().put(0x7F, 0x000186AA); + + HuaweiPacket packetOk = new HuaweiPacket(secretsProvider).parse(rawOk); + HuaweiPacket packetErr = new HuaweiPacket(secretsProvider).parse(rawErr); + packetOk.parseTlv(); + packetErr.parseTlv(); + + Assert.assertEquals(0x25, packetOk.serviceId); + Assert.assertEquals(0x01, packetOk.commandId); + Assert.assertEquals(expectedTlvOk, tlvField.get(packetOk)); + Assert.assertTrue(packetOk instanceof MusicControl.MusicStatusResponse); + Assert.assertEquals(0x000186A0, ((MusicControl.MusicStatusResponse) packetOk).status); + + Assert.assertEquals(0x25, packetErr.serviceId); + Assert.assertEquals(0x01, packetErr.commandId); + Assert.assertEquals(expectedTlvErr, tlvField.get(packetErr)); + Assert.assertTrue(packetErr instanceof MusicControl.MusicStatusResponse); + Assert.assertEquals(0x000186AA, ((MusicControl.MusicStatusResponse) packetErr).status); + } + + @Test + public void testMusicInfoRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + String artistName = "Artist"; + String songName = "Song"; + byte playState = 0x01; + byte maxVolume = 0x03; + byte currentVolume = 0x02; + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x01, artistName) + .put(0x02, songName) + .put(0x03, playState) + .put(0x04, maxVolume) + .put(0x05, currentVolume); + byte[] expectedSerializedPacket = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x3a, (byte) 0x00, (byte) 0x25, (byte) 0x02, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x20, (byte) 0x21, (byte) 0x8b, (byte) 0xe1, (byte) 0x3d, (byte) 0x9f, (byte) 0x85, (byte) 0xd2, (byte) 0x2e, (byte) 0x64, (byte) 0x87, (byte) 0x3f, (byte) 0x1d, (byte) 0xab, (byte) 0x3f, (byte) 0xc7, (byte) 0x39, (byte) 0xb6, (byte) 0x34, (byte) 0x89, (byte) 0x60, (byte) 0xa0, (byte) 0x36, (byte) 0x4a, (byte) 0x08, (byte) 0x7a, (byte) 0x16, (byte) 0xed, (byte) 0xc9, (byte) 0x9e, (byte) 0xf3, (byte) 0xbf, (byte) 0x44, (byte) 0xac, (byte) 0x58}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + MusicControl.MusicInfo.Request musicInfoRequest = new MusicControl.MusicInfo.Request( + secretsProvider, + artistName, + songName, + playState, + maxVolume, + currentVolume + ); + + Assert.assertEquals(0x25, musicInfoRequest.serviceId); + Assert.assertEquals(0x02, musicInfoRequest.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(musicInfoRequest)); + Assert.assertTrue(musicInfoRequest.complete); + List out = musicInfoRequest.serialize(); + Assert.assertEquals(1, out.size()); + Assert.assertArrayEquals(expectedSerializedPacket, out.get(0)); + } + + @Test + public void testMusicInfoResponse() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] rawOk = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x09, (byte) 0x00, (byte) 0x25, (byte) 0x02, (byte) 0x7f, (byte) 0x04, (byte) 0x00, (byte) 0x01, (byte) 0x86, (byte) 0xA0, (byte) 0xbb, (byte) 0x14}; + byte[] rawErr = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x09, (byte) 0x00, (byte) 0x25, (byte) 0x02, (byte) 0x7f, (byte) 0x04, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x88, (byte) 0xf0}; + byte[] rawMissing = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x03, (byte) 0x00, (byte) 0x25, (byte) 0x02, (byte) 0xb4, (byte) 0x1b}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV okExpectedTlv = new HuaweiTLV() + .put(0x7f, 0x000186a0); + HuaweiTLV errExpectedTlv = new HuaweiTLV() + .put(0x7f, 0x00000000); + HuaweiTLV missingExpectedTlv = new HuaweiTLV(); + + HuaweiPacket packetOk = new HuaweiPacket(secretsProvider).parse(rawOk); + HuaweiPacket packetErr = new HuaweiPacket(secretsProvider).parse(rawErr); + HuaweiPacket packetMissing = new HuaweiPacket(secretsProvider).parse(rawMissing); + + packetOk.parseTlv(); + packetErr.parseTlv(); + packetMissing.parseTlv(); + + Assert.assertEquals(0x25, packetOk.serviceId); + Assert.assertEquals(0x02, packetOk.commandId); + Assert.assertEquals(okExpectedTlv, tlvField.get(packetOk)); + Assert.assertTrue(packetOk instanceof MusicControl.MusicInfo.Response); + Assert.assertTrue(((MusicControl.MusicInfo.Response) packetOk).ok); + Assert.assertEquals("", ((MusicControl.MusicInfo.Response) packetOk).error); + + Assert.assertEquals(0x25, packetErr.serviceId); + Assert.assertEquals(0x02, packetErr.commandId); + Assert.assertEquals(errExpectedTlv, tlvField.get(packetErr)); + Assert.assertTrue(packetErr instanceof MusicControl.MusicInfo.Response); + Assert.assertFalse(((MusicControl.MusicInfo.Response) packetErr).ok); + Assert.assertEquals("Music information error code: 0", ((MusicControl.MusicInfo.Response) packetErr).error); + + Assert.assertEquals(0x25, packetMissing.serviceId); + Assert.assertEquals(0x02, packetMissing.commandId); + Assert.assertEquals(missingExpectedTlv, tlvField.get(packetMissing)); + Assert.assertTrue(packetMissing instanceof MusicControl.MusicInfo.Response); + Assert.assertFalse(((MusicControl.MusicInfo.Response) packetMissing).ok); + Assert.assertEquals("Music information response no status tag", ((MusicControl.MusicInfo.Response) packetMissing).error); + } + + @Test + public void testControlResponse() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] emptyInput = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x03, (byte) 0x00, (byte) 0x25, (byte) 0x03, (byte) 0xa4, (byte) 0x3a, }; + byte[] playPauseInput = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x06, (byte) 0x00, (byte) 0x25, (byte) 0x03, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0xe6, (byte) 0x85}; + byte[] previousInput = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x06, (byte) 0x00, (byte) 0x25, (byte) 0x03, (byte) 0x01, (byte) 0x01, (byte) 0x03, (byte) 0xc6, (byte) 0xc7}; + byte[] nextInput = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x06, (byte) 0x00, (byte) 0x25, (byte) 0x03, (byte) 0x01, (byte) 0x01, (byte) 0x04, (byte) 0xb6, (byte) 0x20}; + byte[] unknownButtonInput = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x06, (byte) 0x00, (byte) 0x25, (byte) 0x03, (byte) 0x01, (byte) 0x01, (byte) 0xFF, (byte) 0xe8, (byte) 0x54}; + byte[] volumeInput = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x06, (byte) 0x00, (byte) 0x25, (byte) 0x03, (byte) 0x02, (byte) 0x01, (byte) 0x42, (byte) 0xc7, (byte) 0x72}; + byte[] combinedInput = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x09, (byte) 0x00, (byte) 0x25, (byte) 0x03, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x02, (byte) 0x01, (byte) 0x42, (byte) 0x95, (byte) 0x9a}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV emptyExpectedTlv = new HuaweiTLV(); + HuaweiTLV playPauseExpectedTlv = new HuaweiTLV() + .put(0x01, (byte) 0x01); + HuaweiTLV previousExpectedTlv = new HuaweiTLV() + .put(0x01, (byte) 0x03); + HuaweiTLV nextExpectedTlv = new HuaweiTLV() + .put(0x01, (byte) 0x04); + HuaweiTLV unknownButtonExpectedTlv = new HuaweiTLV() + .put(0x01, (byte) 0xFF); + HuaweiTLV volumeExpectedTlv = new HuaweiTLV() + .put(0x02, (byte) 0x42); + HuaweiTLV combinedExpectedTlv = new HuaweiTLV() + .put(0x01, (byte) 0x01) + .put(0x02, (byte) 0x42); + + HuaweiPacket emptyResponse = new HuaweiPacket(secretsProvider).parse(emptyInput); + HuaweiPacket playPauseResponse = new HuaweiPacket(secretsProvider).parse(playPauseInput); + HuaweiPacket previousResponse = new HuaweiPacket(secretsProvider).parse(previousInput); + HuaweiPacket nextResponse = new HuaweiPacket(secretsProvider).parse(nextInput); + HuaweiPacket unknownButtonResponse = new HuaweiPacket(secretsProvider).parse(unknownButtonInput); + HuaweiPacket volumeResponse = new HuaweiPacket(secretsProvider).parse(volumeInput); + HuaweiPacket combinedResponse = new HuaweiPacket(secretsProvider).parse(combinedInput); + + emptyResponse.parseTlv(); + playPauseResponse.parseTlv(); + previousResponse.parseTlv(); + nextResponse.parseTlv(); + unknownButtonResponse.parseTlv(); + volumeResponse.parseTlv(); + combinedResponse.parseTlv(); + + // TODO: play and pause are now split - test needs to be updated + + Assert.assertEquals(0x25, emptyResponse.serviceId); + Assert.assertEquals(0x03, emptyResponse.commandId); + Assert.assertEquals(emptyExpectedTlv, tlvField.get(emptyResponse)); + Assert.assertTrue(emptyResponse instanceof MusicControl.Control.Response); + Assert.assertFalse(((MusicControl.Control.Response) emptyResponse).buttonPresent); + Assert.assertFalse(((MusicControl.Control.Response) emptyResponse).volumePresent); + + Assert.assertEquals(0x25, playPauseResponse.serviceId); + Assert.assertEquals(0x03, playPauseResponse.commandId); + Assert.assertEquals(playPauseExpectedTlv, tlvField.get(playPauseResponse)); + Assert.assertTrue(playPauseResponse instanceof MusicControl.Control.Response); + Assert.assertTrue(((MusicControl.Control.Response) playPauseResponse).buttonPresent); + Assert.assertEquals(0x01, ((MusicControl.Control.Response) playPauseResponse).rawButton); + // Assert.assertEquals(Button.PlayPause, ((MusicControl.Control.Response) playPauseResponse).button); + Assert.assertFalse(((MusicControl.Control.Response) playPauseResponse).volumePresent); + + Assert.assertEquals(0x25, previousResponse.serviceId); + Assert.assertEquals(0x03, previousResponse.commandId); + Assert.assertEquals(previousExpectedTlv, tlvField.get(previousResponse)); + Assert.assertTrue(previousResponse instanceof MusicControl.Control.Response); + Assert.assertTrue(((MusicControl.Control.Response) previousResponse).buttonPresent); + Assert.assertEquals(0x03, ((MusicControl.Control.Response) previousResponse).rawButton); + Assert.assertEquals(Button.Previous, ((MusicControl.Control.Response) previousResponse).button); + Assert.assertFalse(((MusicControl.Control.Response) previousResponse).volumePresent); + + Assert.assertEquals(0x25, nextResponse.serviceId); + Assert.assertEquals(0x03, nextResponse.commandId); + Assert.assertEquals(nextExpectedTlv, tlvField.get(nextResponse)); + Assert.assertTrue(nextResponse instanceof MusicControl.Control.Response); + Assert.assertTrue(((MusicControl.Control.Response) nextResponse).buttonPresent); + Assert.assertEquals(0x04, ((MusicControl.Control.Response) nextResponse).rawButton); + Assert.assertEquals(Button.Next, ((MusicControl.Control.Response) nextResponse).button); + Assert.assertFalse(((MusicControl.Control.Response) nextResponse).volumePresent); + + Assert.assertEquals(0x25, unknownButtonResponse.serviceId); + Assert.assertEquals(0x03, unknownButtonResponse.commandId); + Assert.assertEquals(unknownButtonExpectedTlv, tlvField.get(unknownButtonResponse)); + Assert.assertTrue(unknownButtonResponse instanceof MusicControl.Control.Response); + Assert.assertTrue(((MusicControl.Control.Response) unknownButtonResponse).buttonPresent); + Assert.assertEquals((byte) 0xFF, ((MusicControl.Control.Response) unknownButtonResponse).rawButton); + Assert.assertEquals(Button.Unknown, ((MusicControl.Control.Response) unknownButtonResponse).button); + Assert.assertFalse(((MusicControl.Control.Response) unknownButtonResponse).volumePresent); + + Assert.assertEquals(0x25, volumeResponse.serviceId); + Assert.assertEquals(0x03, volumeResponse.commandId); + Assert.assertEquals(volumeExpectedTlv, tlvField.get(volumeResponse)); + Assert.assertTrue(volumeResponse instanceof MusicControl.Control.Response); + Assert.assertFalse(((MusicControl.Control.Response) volumeResponse).buttonPresent); + Assert.assertTrue(((MusicControl.Control.Response) volumeResponse).volumePresent); + Assert.assertEquals(0x42, ((MusicControl.Control.Response) volumeResponse).volume); + + Assert.assertEquals(0x25, combinedResponse.serviceId); + Assert.assertEquals(0x03, combinedResponse.commandId); + Assert.assertEquals(combinedExpectedTlv, tlvField.get(combinedResponse)); + Assert.assertTrue(combinedResponse instanceof MusicControl.Control.Response); + Assert.assertTrue(((MusicControl.Control.Response) combinedResponse).buttonPresent); + Assert.assertEquals(0x01, ((MusicControl.Control.Response) combinedResponse).rawButton); + // Assert.assertEquals(Button.PlayPause, ((MusicControl.Control.Response) combinedResponse).button); + Assert.assertTrue(((MusicControl.Control.Response) combinedResponse).volumePresent); + Assert.assertEquals(0x42, ((MusicControl.Control.Response) combinedResponse).volume); + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestNotifications.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestNotifications.java new file mode 100644 index 000000000..fb42a7c8e --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestNotifications.java @@ -0,0 +1,199 @@ +/* Copyright (C) 2022-2023 Martin.JM + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV; + +public class TestNotifications { + + HuaweiPacket.ParamsProvider secretsProvider = new HuaweiPacket.ParamsProvider() { + @Override + public byte getDeviceSupportType() { + return 0; + } + + @Override + public byte[] getSecretKey() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public byte[] getIv() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public boolean areTransactionsCrypted() { + return true; + } + + @Override + public int getMtu() { + return 0; + } + + @Override + public int getSliceSize() { + return 0xF4; + } + }; + + @Test + public void testNotificationActionRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + short notificationId = 0x01; + byte notificationType = 0x02; + byte titleEncoding = 0x02; + String titleContent = "Title"; + byte senderEncoding = 0x02; + String senderContent = "Sender"; + byte bodyEncoding = 0x02; + String bodyContent = "Body"; + String sourceAppId = "SourceApp"; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV() + .put(0x01, notificationId) + .put(0x02, notificationType) + .put(0x03, true) + .put(0x84, new HuaweiTLV() + .put(0x8C, new HuaweiTLV() + .put(0x8D, new HuaweiTLV() + .put(0x0E, (byte) 0x03) + .put(0x0F, titleEncoding) + .put(0x10, titleContent) + ) + .put(0x8D, new HuaweiTLV() + .put(0x0E, (byte) 0x02) + .put(0x0F, senderEncoding) + .put(0x10, senderContent) + ) + .put(0x8D, new HuaweiTLV() + .put(0x0E, (byte) 0x01) + .put(0x0F, bodyEncoding) + .put(0x10, bodyContent) + ) + ) + ) + .put(0x11, sourceAppId); + + Notifications.NotificationActionRequest request = new Notifications.NotificationActionRequest( + secretsProvider, + notificationId, + notificationType, + titleEncoding, + titleContent, + senderEncoding, + senderContent, + bodyEncoding, + bodyContent, + sourceAppId + ); + + Assert.assertEquals(0x02, request.serviceId); + Assert.assertEquals(0x01, request.commandId); + Assert.assertTrue(request.complete); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + + // Only check that this doesn't error + request.serialize(); + } + + @Test + public void testSetNotificationRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlvTrue = new HuaweiTLV() + .put(0x81, new HuaweiTLV() + .put(0x02, true) + .put(0x03, true) + ); + + HuaweiTLV expectedTlvFalse = new HuaweiTLV() + .put(0x81, new HuaweiTLV() + .put(0x02, false) + .put(0x03, false) + ); + + byte[] expectedOutputTrue = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x02, (byte) 0x04, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0xd9, (byte) 0xc4, (byte) 0xaa, (byte) 0x7d, (byte) 0xa3, (byte) 0x5c, (byte) 0x42, (byte) 0xab, (byte) 0x2d, (byte) 0xc2, (byte) 0xe7, (byte) 0x73, (byte) 0xc0, (byte) 0x4c, (byte) 0x97, (byte) 0x5a, (byte) 0x41, (byte) 0x23}; + byte[] expectedOutputFalse = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x02, (byte) 0x04, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0xeb, (byte) 0x1f, (byte) 0x20, (byte) 0x0a, (byte) 0x7d, (byte) 0xe2, (byte) 0x25, (byte) 0x45, (byte) 0x01, (byte) 0x5b, (byte) 0xe8, (byte) 0x24, (byte) 0xe3, (byte) 0x7e, (byte) 0x1d, (byte) 0x9c, (byte) 0x47, (byte) 0x31}; + + Notifications.NotificationStateRequest requestTrue = new Notifications.NotificationStateRequest(secretsProvider, true); + Notifications.NotificationStateRequest requestFalse = new Notifications.NotificationStateRequest(secretsProvider, false); + + Assert.assertEquals(0x02, requestTrue.serviceId); + Assert.assertEquals(0x04, requestTrue.commandId); + Assert.assertTrue(requestTrue.complete); + Assert.assertEquals(expectedTlvTrue, tlvField.get(requestTrue)); + List outTrue = requestTrue.serialize(); + Assert.assertEquals(1, outTrue.size()); + Assert.assertArrayEquals(expectedOutputTrue, outTrue.get(0)); + + Assert.assertEquals(0x02, requestFalse.serviceId); + Assert.assertEquals(0x04, requestFalse.commandId); + Assert.assertTrue(requestFalse.complete); + Assert.assertEquals(expectedTlvFalse, tlvField.get(requestFalse)); + List outFalse = requestFalse.serialize(); + Assert.assertEquals(1, outFalse.size()); + Assert.assertArrayEquals(expectedOutputFalse, outFalse.get(0)); + } + + @Test + @Ignore("Broken since https://codeberg.org/psolyca/Gadgetbridge/commit/5b0736b7518aa5c998ac13207fff66286393965b") + public void testSetWearMessagePushRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlvTrue = new HuaweiTLV() + .put(0x01, true); + + HuaweiTLV expectedTlvFalse = new HuaweiTLV() + .put(0x01, false); + + byte[] expectedOutputTrue = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x02, (byte) 0x08, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0xcd, (byte) 0x97, (byte) 0x7e, (byte) 0x01, (byte) 0x48, (byte) 0x34, (byte) 0x2a, (byte) 0x48, (byte) 0x58, (byte) 0x0d, (byte) 0x30, (byte) 0xc7, (byte) 0xbc, (byte) 0x2e, (byte) 0x40, (byte) 0xd4, (byte) 0x29, (byte) 0xe0}; + byte[] expectedOutputFalse = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x02, (byte) 0x08, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0x28, (byte) 0x00, (byte) 0x99, (byte) 0x6f, (byte) 0x2a, (byte) 0xcb, (byte) 0x62, (byte) 0x3a, (byte) 0xe6, (byte) 0x54, (byte) 0x28, (byte) 0x54, (byte) 0xf8, (byte) 0xab, (byte) 0x54, (byte) 0x83, (byte) 0x30, (byte) 0xd2}; + + Notifications.WearMessagePushRequest requestTrue = new Notifications.WearMessagePushRequest(secretsProvider, true); + Notifications.WearMessagePushRequest requestFalse = new Notifications.WearMessagePushRequest(secretsProvider, false); + + Assert.assertEquals(0x02, requestTrue.serviceId); + Assert.assertEquals(0x08, requestTrue.commandId); + Assert.assertTrue(requestTrue.complete); + Assert.assertEquals(expectedTlvTrue, tlvField.get(requestTrue)); + List outTrue = requestTrue.serialize(); + Assert.assertEquals(1, outTrue.size()); + Assert.assertArrayEquals(expectedOutputTrue, outTrue.get(0)); + + Assert.assertEquals(0x02, requestFalse.serviceId); + Assert.assertEquals(0x08, requestFalse.commandId); + Assert.assertTrue(requestFalse.complete); + Assert.assertEquals(expectedTlvFalse, tlvField.get(requestFalse)); + List outFalse = requestFalse.serialize(); + Assert.assertEquals(1, outFalse.size()); + Assert.assertArrayEquals(expectedOutputFalse, outFalse.get(0)); + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestWorkMode.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestWorkMode.java new file mode 100644 index 000000000..2be8d6cf6 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestWorkMode.java @@ -0,0 +1,94 @@ +/* Copyright (C) 2022 Martin.JM + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets; + +import org.junit.Assert; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV; + +public class TestWorkMode { + + HuaweiPacket.ParamsProvider secretsProvider = new HuaweiPacket.ParamsProvider() { + @Override + public byte getDeviceSupportType() { + return 0; + } + + @Override + public byte[] getSecretKey() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public byte[] getIv() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public boolean areTransactionsCrypted() { + return true; + } + + @Override + public int getMtu() { + return 0; + } + + @Override + public int getSliceSize() { + return 0xF4; + } + }; + + @Test + public void testSwitchStatusRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlvTrue = new HuaweiTLV() + .put(0x01, true); + HuaweiTLV expectedTlvFalse = new HuaweiTLV() + .put(0x01, false); + + byte[] serializedTrue = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x26, (byte) 0x02, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0xcd, (byte) 0x97, (byte) 0x7e, (byte) 0x01, (byte) 0x48, (byte) 0x34, (byte) 0x2a, (byte) 0x48, (byte) 0x58, (byte) 0x0d, (byte) 0x30, (byte) 0xc7, (byte) 0xbc, (byte) 0x2e, (byte) 0x40, (byte) 0xd4, (byte) 0x5c, (byte) 0x5a}; + byte[] serializedFalse = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x26, (byte) 0x02, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0x28, (byte) 0x00, (byte) 0x99, (byte) 0x6f, (byte) 0x2a, (byte) 0xcb, (byte) 0x62, (byte) 0x3a, (byte) 0xe6, (byte) 0x54, (byte) 0x28, (byte) 0x54, (byte) 0xf8, (byte) 0xab, (byte) 0x54, (byte) 0x83, (byte) 0x45, (byte) 0x68}; + + WorkMode.SwitchStatusRequest requestTrue = new WorkMode.SwitchStatusRequest(secretsProvider, true); + WorkMode.SwitchStatusRequest requestFalse = new WorkMode.SwitchStatusRequest(secretsProvider, false); + + Assert.assertEquals(0x26, requestTrue.serviceId); + Assert.assertEquals(0x02, requestTrue.commandId); + Assert.assertEquals(expectedTlvTrue, tlvField.get(requestTrue)); + Assert.assertTrue(requestTrue.complete); + List outTrue = requestTrue.serialize(); + Assert.assertEquals(1, outTrue.size()); + Assert.assertArrayEquals(serializedTrue, outTrue.get(0)); + + Assert.assertEquals(0x26, requestFalse.serviceId); + Assert.assertEquals(0x02, requestFalse.commandId); + Assert.assertEquals(expectedTlvFalse, tlvField.get(requestFalse)); + Assert.assertTrue(requestFalse.complete); + List outFalse = requestFalse.serialize(); + Assert.assertEquals(1, outFalse.size()); + Assert.assertArrayEquals(serializedFalse, outFalse.get(0)); + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestWorkout.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestWorkout.java new file mode 100644 index 000000000..d29b173de --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/TestWorkout.java @@ -0,0 +1,437 @@ +/* Copyright (C) 2022 Martin.JM + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets; + +import org.junit.Assert; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.nio.ByteBuffer; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV; + +public class TestWorkout { + + HuaweiPacket.ParamsProvider secretsProvider = new HuaweiPacket.ParamsProvider() { + @Override + public byte getDeviceSupportType() { + return 0; + } + + @Override + public byte[] getSecretKey() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public byte[] getIv() { + return new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + + @Override + public boolean areTransactionsCrypted() { + return true; + } + + @Override + public int getMtu() { + return 0; + } + + @Override + public int getSliceSize() { + return 0xF4; + } + }; + + @Test + public void testWorkoutCountRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + int start = 0x00000000; + int end = 0x01020304; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x81, new HuaweiTLV() + .put(0x03, start) + .put(0x04, end) + ); + + byte[] expected = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x17, (byte) 0x07, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0xf7, (byte) 0x48, (byte) 0xf7, (byte) 0x49, (byte) 0x4a, (byte) 0xa5, (byte) 0xb2, (byte) 0xc9, (byte) 0x41, (byte) 0xf5, (byte) 0x7f, (byte) 0xb4, (byte) 0xe9, (byte) 0x17, (byte) 0xac, (byte) 0xb5, (byte) 0x5f, (byte) 0x8e}; + + Workout.WorkoutCount.Request request = new Workout.WorkoutCount.Request(secretsProvider, start, end); + + Assert.assertEquals(0x17, request.serviceId); + Assert.assertEquals(0x07, request.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + Assert.assertTrue(request.complete); + List out = request.serialize(); + Assert.assertEquals(1, out.size()); + Assert.assertArrayEquals(expected, out.get(0)); + } + + @Test + public void testWorkoutCountResponse() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] raw = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x4a, (byte) 0x00, (byte) 0x17, (byte) 0x07, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x30, (byte) 0xee, (byte) 0xdd, (byte) 0xa9, (byte) 0x23, (byte) 0x2c, (byte) 0xe4, (byte) 0x9f, (byte) 0x41, (byte) 0x0b, (byte) 0x9f, (byte) 0x7a, (byte) 0xc2, (byte) 0xe0, (byte) 0x72, (byte) 0x6d, (byte) 0xe1, (byte) 0x8f, (byte) 0xd0, (byte) 0xe7, (byte) 0x41, (byte) 0x59, (byte) 0x38, (byte) 0xac, (byte) 0x17, (byte) 0x66, (byte) 0xc8, (byte) 0x60, (byte) 0xd7, (byte) 0xd2, (byte) 0x32, (byte) 0x8b, (byte) 0xa5, (byte) 0x91, (byte) 0xc7, (byte) 0xc5, (byte) 0xe5, (byte) 0x7d, (byte) 0x8d, (byte) 0xa1, (byte) 0xd0, (byte) 0x6f, (byte) 0xe2, (byte) 0xe2, (byte) 0x24, (byte) 0x7d, (byte) 0xef, (byte) 0x02, (byte) 0x03, (byte) 0x59, (byte) 0x3e}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x81, new HuaweiTLV() + .put(0x02, (short) 0x1337) + .put(0x85, new HuaweiTLV() + .put(0x06, (short) 0x0001) + .put(0x07, (short) 0x0002) + .put(0x08, (short) 0x0003) + .put(0x0a, (short) 0x0004) + ) + .put(0x85, new HuaweiTLV() + .put(0x06, (short) 0x0005) + .put(0x07, (short) 0x0006) + .put(0x08, (short) 0x0007) + .put(0x0a, (short) 0x0008) + ) + ); + + HuaweiPacket packet = new HuaweiPacket(secretsProvider).parse(raw); + packet.parseTlv(); + + Assert.assertEquals(0x17, packet.serviceId); + Assert.assertEquals(0x07, packet.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packet)); + Assert.assertTrue(packet.complete); + Assert.assertTrue(packet instanceof Workout.WorkoutCount.Response); + Assert.assertEquals(0x1337, ((Workout.WorkoutCount.Response) packet).count); + Assert.assertEquals(2, ((Workout.WorkoutCount.Response) packet).workoutNumbers.size()); + + Assert.assertArrayEquals(new byte[] {0x06, 0x02, 0x00, 0x01, 0x07, 0x02, 0x00, 0x02, 0x08, 0x02, 0x00, 0x03, 0x0a, 0x02, 0x00, 0x04}, ((Workout.WorkoutCount.Response) packet).workoutNumbers.get(0).rawData); + Assert.assertEquals(0x01, ((Workout.WorkoutCount.Response) packet).workoutNumbers.get(0).workoutNumber); + Assert.assertEquals(0x02, ((Workout.WorkoutCount.Response) packet).workoutNumbers.get(0).dataCount); + Assert.assertEquals(0x03, ((Workout.WorkoutCount.Response) packet).workoutNumbers.get(0).paceCount); + + Assert.assertArrayEquals(new byte[] {0x06, 0x02, 0x00, 0x05, 0x07, 0x02, 0x00, 0x06, 0x08, 0x02, 0x00, 0x07, 0x0a, 0x02, 0x00, 0x08}, ((Workout.WorkoutCount.Response) packet).workoutNumbers.get(1).rawData); + Assert.assertEquals(0x05, ((Workout.WorkoutCount.Response) packet).workoutNumbers.get(1).workoutNumber); + Assert.assertEquals(0x06, ((Workout.WorkoutCount.Response) packet).workoutNumbers.get(1).dataCount); + Assert.assertEquals(0x07, ((Workout.WorkoutCount.Response) packet).workoutNumbers.get(1).paceCount); + } + + @Test + public void testWorkoutTotalsRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + short number = 0x1337; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x81, new HuaweiTLV() + .put(0x02, number) + ); + + byte[] expected = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x17, (byte) 0x08, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0xf6, (byte) 0xfb, (byte) 0xc0, (byte) 0xb6, (byte) 0x4f, (byte) 0x9a, (byte) 0xfa, (byte) 0x77, (byte) 0x53, (byte) 0x28, (byte) 0x7d, (byte) 0x13, (byte) 0xca, (byte) 0x49, (byte) 0xda, (byte) 0xfd, (byte) 0x26, (byte) 0x91}; + + Workout.WorkoutTotals.Request request = new Workout.WorkoutTotals.Request(secretsProvider, number); + + Assert.assertEquals(0x17, request.serviceId); + Assert.assertEquals(0x08, request.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + Assert.assertTrue(request.complete); + List out = request.serialize(); + Assert.assertEquals(1, out.size()); + Assert.assertArrayEquals(expected, out.get(0)); + } + + @Test + public void testWorkoutTotalsResponse() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] raw = new byte[] {(byte) 0x5a, (byte) 0x00, (byte) 0x5a, (byte) 0x00, (byte) 0x17, (byte) 0x08, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x40, (byte) 0x0f, (byte) 0xa0, (byte) 0x3a, (byte) 0x90, (byte) 0xae, (byte) 0x8c, (byte) 0xcf, (byte) 0x03, (byte) 0xce, (byte) 0x5a, (byte) 0x68, (byte) 0x87, (byte) 0x05, (byte) 0x51, (byte) 0xf7, (byte) 0x2f, (byte) 0x78, (byte) 0xbd, (byte) 0x84, (byte) 0xf1, (byte) 0x4f, (byte) 0xb8, (byte) 0x51, (byte) 0x28, (byte) 0xec, (byte) 0xfd, (byte) 0x8b, (byte) 0x2e, (byte) 0x99, (byte) 0xd3, (byte) 0x42, (byte) 0xd7, (byte) 0x65, (byte) 0xb2, (byte) 0x82, (byte) 0x02, (byte) 0x28, (byte) 0x00, (byte) 0x34, (byte) 0xbc, (byte) 0x39, (byte) 0x59, (byte) 0x8f, (byte) 0x0b, (byte) 0xa7, (byte) 0x3a, (byte) 0x5c, (byte) 0xfb, (byte) 0xf1, (byte) 0xd4, (byte) 0x8f, (byte) 0xf6, (byte) 0x6d, (byte) 0x98, (byte) 0xd6, (byte) 0x5a, (byte) 0x51, (byte) 0x0a, (byte) 0x4a, (byte) 0x1c, (byte) 0x42, (byte) 0xc8, (byte) 0x9d, (byte) 0xee, (byte) 0x55, (byte) 0x44}; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x81, new HuaweiTLV() + .put(0x02, (short) 0x1337) + .put(0x03, (byte) 0x01) + .put(0x04, 0x01020304) + .put(0x05, 0x05060708) + .put(0x06, 0x090a0b0c) + .put(0x07, 0x0d0e0f10) + .put(0x08, 0x11121314) + .put(0x09, 0x15161718) + .put(0x12, 0x191a1b1c) + .put(0x14, (byte) 0x1d) + ); + + HuaweiPacket packet = new HuaweiPacket(secretsProvider).parse(raw); + packet.parseTlv(); + + // TODO: find out what the status and type can be + + Assert.assertEquals(0x17, packet.serviceId); + Assert.assertEquals(0x08, packet.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packet)); + Assert.assertTrue(packet.complete); + Assert.assertTrue(packet instanceof Workout.WorkoutTotals.Response); + Assert.assertArrayEquals(new byte[] {0x02, 0x02, 0x13, 0x37, 0x03, 0x01, 0x01, 0x04, 0x04, 0x01, 0x02, 0x03, 0x04, 0x05, 0x04, 0x05, 0x06, 0x07, 0x08, 0x06, 0x04, 0x09, 0x0a, 0x0b, 0x0c, 0x07, 0x04, 0x0d, 0x0e, 0x0f, 0x10, 0x08, 0x04, 0x11, 0x12, 0x13, 0x14, 0x09, 0x04, 0x15, 0x16, 0x17, 0x18, 0x12, 0x04, 0x19, 0x1a, 0x1b, 0x1c, 0x14, 0x01, 0x1d}, ((Workout.WorkoutTotals.Response) packet).rawData); + Assert.assertEquals(0x1337, ((Workout.WorkoutTotals.Response) packet).number); + Assert.assertEquals(0x01, ((Workout.WorkoutTotals.Response) packet).status); + Assert.assertEquals(0x01020304, ((Workout.WorkoutTotals.Response) packet).startTime); + Assert.assertEquals(0x05060708, ((Workout.WorkoutTotals.Response) packet).endTime); + Assert.assertEquals(0x090a0b0c, ((Workout.WorkoutTotals.Response) packet).calories); + Assert.assertEquals(0x0d0e0f10, ((Workout.WorkoutTotals.Response) packet).distance); + Assert.assertEquals(0x11121314, ((Workout.WorkoutTotals.Response) packet).stepCount); + Assert.assertEquals(0x15161718, ((Workout.WorkoutTotals.Response) packet).totalTime); + Assert.assertEquals(0x191a1b1c, ((Workout.WorkoutTotals.Response) packet).duration); + Assert.assertEquals(0x1d, ((Workout.WorkoutTotals.Response) packet).type); + } + + @Test + public void testWorkoutDataRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + short workoutNumber = 0x0102; + short dataNumber = 0x0304; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x81, new HuaweiTLV() + .put(0x02, workoutNumber) + .put(0x03, dataNumber) + ); + + byte[] expected = {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x17, (byte) 0x0a, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0xd2, (byte) 0xd7, (byte) 0x55, (byte) 0x23, (byte) 0xeb, (byte) 0x51, (byte) 0x4f, (byte) 0xe0, (byte) 0x35, (byte) 0x6c, (byte) 0x60, (byte) 0xc5, (byte) 0xbf, (byte) 0x61, (byte) 0x68, (byte) 0xd1, (byte) 0x03, (byte) 0x83}; + + Workout.WorkoutData.Request request = new Workout.WorkoutData.Request(secretsProvider, workoutNumber, dataNumber); + + Assert.assertEquals(0x17, request.serviceId); + Assert.assertEquals(0x0a, request.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + Assert.assertTrue(request.complete); + List out = request.serialize(); + Assert.assertEquals(1, out.size()); + Assert.assertArrayEquals(expected, out.get(0)); + } + + @Test + public void testWorkoutDataResponse() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] raw = {(byte) 0x5a, (byte) 0x00, (byte) 0x5a, (byte) 0x00, (byte) 0x17, (byte) 0x0a, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x40, (byte) 0x03, (byte) 0x66, (byte) 0xf5, (byte) 0x16, (byte) 0xc9, (byte) 0x60, (byte) 0xb9, (byte) 0xf2, (byte) 0xe3, (byte) 0x88, (byte) 0x99, (byte) 0xab, (byte) 0x50, (byte) 0x22, (byte) 0xcb, (byte) 0x83, (byte) 0x53, (byte) 0xd0, (byte) 0xb2, (byte) 0xc3, (byte) 0x66, (byte) 0xa9, (byte) 0x16, (byte) 0x23, (byte) 0xa5, (byte) 0x8e, (byte) 0x81, (byte) 0x68, (byte) 0x85, (byte) 0x38, (byte) 0x3e, (byte) 0xd5, (byte) 0x8e, (byte) 0x21, (byte) 0xc8, (byte) 0xa1, (byte) 0x80, (byte) 0x98, (byte) 0x2d, (byte) 0x78, (byte) 0x75, (byte) 0x80, (byte) 0xa1, (byte) 0x39, (byte) 0x61, (byte) 0xa6, (byte) 0x3e, (byte) 0x61, (byte) 0x2c, (byte) 0x5e, (byte) 0xe2, (byte) 0x6f, (byte) 0xef, (byte) 0xdf, (byte) 0xdb, (byte) 0x39, (byte) 0x8f, (byte) 0xab, (byte) 0x21, (byte) 0xde, (byte) 0xba, (byte) 0xdb, (byte) 0x2c, (byte) 0xff, (byte) 0x97, (byte) 0x94}; + + short workoutNumber = 0x0102; + short dataNumber = 0x0304; + + int timestamp = 0x05060708; + byte interval = 0x09; + short dataCount = 0x0002; + byte dataLength = 0x0F; // Data length must match + short bitmap = 0x0042; // Inner data and speed + + short speed1 = 0x0a0b; + short cadence1 = 0x0c0d; + short stepLength1 = 0x0e0f; + short groundContactTime1 = 0x1011; + byte groundImpact1 = 0x12; + short swingAngle1 = 0x1314; + byte foreFootLanding1 = 0x15; + byte midFootLanding1 = 0x16; + byte backFootLanding1 = 0x17; + byte eversionAngle1 = 0x18; + + short speed2 = 0x191a; + short cadence2 = 0x1b1c; + short stepLength2 = 0x1d1e; + short groundContactTime2 = 0x1f20; + byte groundImpact2 = 0x21; + short swingAngle2 = 0x2223; + byte foreFootLanding2 = 0x24; + byte midFootLanding2 = 0x25; + byte backFootLanding2 = 0x26; + byte eversionAngle2 = 0x27; + + ByteBuffer headerBuf = ByteBuffer.allocate(14); + headerBuf.putShort(workoutNumber); + headerBuf.putShort(dataNumber); + headerBuf.putInt(timestamp); + headerBuf.put(interval); + headerBuf.putShort(dataCount); + headerBuf.put(dataLength); + headerBuf.putShort(bitmap); + + ByteBuffer dataBuf = ByteBuffer.allocate(30); + + dataBuf.putShort(speed1); + dataBuf.putShort(cadence1); + dataBuf.putShort(stepLength1); + dataBuf.putShort(groundContactTime1); + dataBuf.put(groundImpact1); + dataBuf.putShort(swingAngle1); + dataBuf.put(foreFootLanding1); + dataBuf.put(midFootLanding1); + dataBuf.put(backFootLanding1); + dataBuf.put(eversionAngle1); + + dataBuf.putShort(speed2); + dataBuf.putShort(cadence2); + dataBuf.putShort(stepLength2); + dataBuf.putShort(groundContactTime2); + dataBuf.put(groundImpact2); + dataBuf.putShort(swingAngle2); + dataBuf.put(foreFootLanding2); + dataBuf.put(midFootLanding2); + dataBuf.put(backFootLanding2); + dataBuf.put(eversionAngle2); + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x81, new HuaweiTLV() + .put(0x02, workoutNumber) + .put(0x03, dataNumber) + .put(0x04, headerBuf.array()) + .put(0x05, dataBuf.array()) + ); + + HuaweiPacket packet = new HuaweiPacket(secretsProvider).parse(raw); + packet.parseTlv(); + + Assert.assertEquals(0x17, packet.serviceId); + Assert.assertEquals(0x0a, packet.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packet)); + Assert.assertTrue(packet.complete); + Assert.assertTrue(packet instanceof Workout.WorkoutData.Response); + + Assert.assertEquals(0x0102, ((Workout.WorkoutData.Response) packet).workoutNumber); + Assert.assertEquals(0x0304, ((Workout.WorkoutData.Response) packet).dataNumber); + Assert.assertArrayEquals(headerBuf.array(), ((Workout.WorkoutData.Response) packet).rawHeader); + Assert.assertArrayEquals(dataBuf.array(), ((Workout.WorkoutData.Response) packet).rawData); + + Assert.assertEquals(0x0102, ((Workout.WorkoutData.Response) packet).header.workoutNumber); + Assert.assertEquals(0x0304, ((Workout.WorkoutData.Response) packet).header.dataNumber); + Assert.assertEquals(0x05060708, ((Workout.WorkoutData.Response) packet).header.timestamp); + Assert.assertEquals(0x09, ((Workout.WorkoutData.Response) packet).header.interval); + Assert.assertEquals(0x0002, ((Workout.WorkoutData.Response) packet).header.dataCount); + Assert.assertEquals(0x0f, ((Workout.WorkoutData.Response) packet).header.dataLength); + Assert.assertEquals(0x0042, ((Workout.WorkoutData.Response) packet).header.bitmap); + + Assert.assertEquals(2, ((Workout.WorkoutData.Response) packet).dataList.size()); + + Assert.assertNull(((Workout.WorkoutData.Response) packet).dataList.get(0).unknownData); + Assert.assertEquals(0x0a0b, ((Workout.WorkoutData.Response) packet).dataList.get(0).speed); + Assert.assertEquals(0x0c0d, ((Workout.WorkoutData.Response) packet).dataList.get(0).cadence); + Assert.assertEquals(0x0e0f, ((Workout.WorkoutData.Response) packet).dataList.get(0).stepLength); + Assert.assertEquals(0x1011, ((Workout.WorkoutData.Response) packet).dataList.get(0).groundContactTime); + Assert.assertEquals(0x12, ((Workout.WorkoutData.Response) packet).dataList.get(0).impact); + Assert.assertEquals(0x1314, ((Workout.WorkoutData.Response) packet).dataList.get(0).swingAngle); + Assert.assertEquals(0x15, ((Workout.WorkoutData.Response) packet).dataList.get(0).foreFootLanding); + Assert.assertEquals(0x16, ((Workout.WorkoutData.Response) packet).dataList.get(0).midFootLanding); + Assert.assertEquals(0x17, ((Workout.WorkoutData.Response) packet).dataList.get(0).backFootLanding); + Assert.assertEquals(0x18, ((Workout.WorkoutData.Response) packet).dataList.get(0).eversionAngle); + + Assert.assertNull(((Workout.WorkoutData.Response) packet).dataList.get(1).unknownData); + Assert.assertEquals(0x191a, ((Workout.WorkoutData.Response) packet).dataList.get(1).speed); + Assert.assertEquals(0x1b1c, ((Workout.WorkoutData.Response) packet).dataList.get(1).cadence); + Assert.assertEquals(0x1d1e, ((Workout.WorkoutData.Response) packet).dataList.get(1).stepLength); + Assert.assertEquals(0x1f20, ((Workout.WorkoutData.Response) packet).dataList.get(1).groundContactTime); + Assert.assertEquals(0x21, ((Workout.WorkoutData.Response) packet).dataList.get(1).impact); + Assert.assertEquals(0x2223, ((Workout.WorkoutData.Response) packet).dataList.get(1).swingAngle); + Assert.assertEquals(0x24, ((Workout.WorkoutData.Response) packet).dataList.get(1).foreFootLanding); + Assert.assertEquals(0x25, ((Workout.WorkoutData.Response) packet).dataList.get(1).midFootLanding); + Assert.assertEquals(0x26, ((Workout.WorkoutData.Response) packet).dataList.get(1).backFootLanding); + Assert.assertEquals(0x27, ((Workout.WorkoutData.Response) packet).dataList.get(1).eversionAngle); + } + + @Test + public void testWorkoutPaceRequest() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.CryptoException { + short workoutNumber = 0x0102; + short paceNumber = 0x0304; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x81, new HuaweiTLV() + .put(0x02, workoutNumber) + .put(0x08, paceNumber) + ); + + byte[] expected = {(byte) 0x5a, (byte) 0x00, (byte) 0x2a, (byte) 0x00, (byte) 0x17, (byte) 0x0c, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x10, (byte) 0x0c, (byte) 0x18, (byte) 0x24, (byte) 0x67, (byte) 0x5e, (byte) 0xe9, (byte) 0x8d, (byte) 0x36, (byte) 0x5f, (byte) 0xde, (byte) 0x1c, (byte) 0x9e, (byte) 0xa0, (byte) 0xd7, (byte) 0x0a, (byte) 0x01, (byte) 0xd3, (byte) 0xce}; + + Workout.WorkoutPace.Request request = new Workout.WorkoutPace.Request(secretsProvider, workoutNumber, paceNumber); + + Assert.assertEquals(0x17, request.serviceId); + Assert.assertEquals(0x0c, request.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(request)); + Assert.assertTrue(request.complete); + List out = request.serialize(); + Assert.assertEquals(1, out.size()); + Assert.assertArrayEquals(expected, out.get(0)); + } + + @Test + public void testWorkoutPaceResponse() throws NoSuchFieldException, IllegalAccessException, HuaweiPacket.ParseException { + byte[] raw = {(byte) 0x5a, (byte) 0x00, (byte) 0x4a, (byte) 0x00, (byte) 0x17, (byte) 0x0c, (byte) 0x7c, (byte) 0x01, (byte) 0x01, (byte) 0x7d, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7e, (byte) 0x30, (byte) 0xe8, (byte) 0xfe, (byte) 0xb9, (byte) 0x27, (byte) 0xa6, (byte) 0xc5, (byte) 0x81, (byte) 0x65, (byte) 0x51, (byte) 0xb8, (byte) 0x24, (byte) 0xfe, (byte) 0x2a, (byte) 0xdc, (byte) 0x3d, (byte) 0x22, (byte) 0xd7, (byte) 0x34, (byte) 0x62, (byte) 0xaf, (byte) 0x06, (byte) 0x5f, (byte) 0xfe, (byte) 0x9c, (byte) 0xe8, (byte) 0xa6, (byte) 0x87, (byte) 0x23, (byte) 0xd6, (byte) 0xc7, (byte) 0x7a, (byte) 0xeb, (byte) 0x07, (byte) 0x06, (byte) 0x5c, (byte) 0x35, (byte) 0xe8, (byte) 0x99, (byte) 0xd3, (byte) 0x96, (byte) 0x0b, (byte) 0x99, (byte) 0x38, (byte) 0x65, (byte) 0x48, (byte) 0xcf, (byte) 0x0f, (byte) 0x99, (byte) 0xe2, (byte) 0x23}; + + short workoutNumber = 0x0102; + short paceNumber = 0x0304; + + short distance1 = 0x0506; + byte type1 = 0x07; + int pace1 = 0x08090a0b; + + short distance2 = 0x0c0d; + byte type2 = 0x0e; + int pace2 = 0x0f101112; + short correction = 0x1314; + + Field tlvField = HuaweiPacket.class.getDeclaredField("tlv"); + tlvField.setAccessible(true); + + HuaweiTLV expectedTlv = new HuaweiTLV().put(0x81, new HuaweiTLV() + .put(0x02, workoutNumber) + .put(0x08, paceNumber) + .put(0x83, new HuaweiTLV() + .put(0x04, distance1) + .put(0x05, type1) + .put(0x06, pace1) + ) + .put(0x83, new HuaweiTLV() + .put(0x04, distance2) + .put(0x05, type2) + .put(0x06, pace2) + .put(0x09, correction) + ) + ); + + HuaweiPacket packet = new HuaweiPacket(secretsProvider).parse(raw); + packet.parseTlv(); + + Assert.assertEquals(0x17, packet.serviceId); + Assert.assertEquals(0x0c, packet.commandId); + Assert.assertEquals(expectedTlv, tlvField.get(packet)); + Assert.assertTrue(packet.complete); + Assert.assertTrue(packet instanceof Workout.WorkoutPace.Response); + Assert.assertEquals(0x0102, ((Workout.WorkoutPace.Response) packet).workoutNumber); + Assert.assertEquals(0x0304, ((Workout.WorkoutPace.Response) packet).paceNumber); + Assert.assertEquals(2, ((Workout.WorkoutPace.Response) packet).blocks.size()); + + Assert.assertEquals(0x0506, ((Workout.WorkoutPace.Response) packet).blocks.get(0).distance); + Assert.assertEquals(0x07, ((Workout.WorkoutPace.Response) packet).blocks.get(0).type); + Assert.assertEquals(0x08090a0b, ((Workout.WorkoutPace.Response) packet).blocks.get(0).pace); + Assert.assertEquals(0, ((Workout.WorkoutPace.Response) packet).blocks.get(0).correction); + + Assert.assertEquals(0x0c0d, ((Workout.WorkoutPace.Response) packet).blocks.get(1).distance); + Assert.assertEquals(0x0e, ((Workout.WorkoutPace.Response) packet).blocks.get(1).type); + Assert.assertEquals(0x0f101112, ((Workout.WorkoutPace.Response) packet).blocks.get(1).pace); + Assert.assertEquals(0x1314, ((Workout.WorkoutPace.Response) packet).blocks.get(1).correction); + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/cmfwatchpro/CmfCommandTest.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/cmfwatchpro/CmfCommandTest.java new file mode 100644 index 000000000..678be855e --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/cmfwatchpro/CmfCommandTest.java @@ -0,0 +1,39 @@ +/* Copyright (C) 2024 JosÊ Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.cmfwatchpro; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +public class CmfCommandTest { + @Test + public void commandEnumCheckNoOverlap() { + // Ensure that no 2 commands overlap in codes + final Map knownCodes = new HashMap<>(); + for (final CmfCommand cmd : CmfCommand.values()) { + final Boolean existingCode = knownCodes.put( + String.format("cmd1=0x%04x cmd2=0x%04x", cmd.getCmd1(), cmd.getCmd2()), + true + ); + assertNull("Commands with overlapping codes", existingCode); + } + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021SupportTest.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/ZeppOsSupportTest.java similarity index 93% rename from app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021SupportTest.java rename to app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/ZeppOsSupportTest.java index 4850c4db5..ae64b1916 100644 --- a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021SupportTest.java +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/ZeppOsSupportTest.java @@ -14,7 +14,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -package nodomain.freeyourgadget.gadgetbridge.service.devices.huami; +package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos; import android.bluetooth.BluetoothGattCharacteristic; import android.content.Context; @@ -35,10 +35,10 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiFWHelper; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WriteAction; -public class Huami2021SupportTest { +public class ZeppOsSupportTest { @Test public void testSetCurrentTimeWithService() { - final Huami2021Support support = createSupport(); + final ZeppOsSupport support = createSupport(); final TransactionBuilder testTransactionBuilder = new TransactionBuilder("test"); support.setCurrentTimeWithService(testTransactionBuilder); @@ -47,8 +47,8 @@ public class Huami2021SupportTest { Assert.assertArrayEquals(new byte[]{-26, 7, 12, 15, 20, 38, 53, 4, 0, 8, 4}, action.getValue()); } - private Huami2021Support createSupport() { - return new Huami2021Support() { + private ZeppOsSupport createSupport() { + return new ZeppOsSupport() { @Override public BluetoothGattCharacteristic getCharacteristic(final UUID uuid) { return new BluetoothGattCharacteristic(null, 0, 0); diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/TestDebugRequestParser.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/TestDebugRequestParser.java new file mode 100644 index 000000000..1b20d1f89 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/TestDebugRequestParser.java @@ -0,0 +1,396 @@ +/* Copyright (C) 2023 Martin.JM + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei; + +import android.bluetooth.BluetoothGattCharacteristic; +import android.content.Context; + +import org.junit.Assert; +import org.junit.Test; + +import java.io.IOException; +import java.util.List; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Workout; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.btbr.Transaction; +import nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.DebugRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.Request; + +public class TestDebugRequestParser { + + HuaweiSupportProvider supportProvider = new HuaweiSupportProvider(new HuaweiLESupport()) { + + @Override + public boolean isBLE() { + return true; + } + + @Override + public Context getContext() { + return null; + } + + @Override + public GBDevice getDevice() { + return null; + } + + @Override + public byte[] getSerial() { + return new byte[0]; + } + + @Override + public String getDeviceMac() { + return null; + } + + @Override + public byte[] getMacAddress() { + return new byte[0]; + } + + @Override + public byte[] getAndroidId() { + return new byte[0]; + } + + @Override + public short getNotificationId() { + return 0; + } + + @Override + public TransactionBuilder createBrTransactionBuilder(String taskName) { + return null; + } + + @Override + public nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder createLeTransactionBuilder(String taskName) { + return null; + } + + @Override + public void performConnected(Transaction transaction) throws IOException { + + } + + @Override + public void performConnected(nodomain.freeyourgadget.gadgetbridge.service.btle.Transaction transaction) throws IOException { + + } + + @Override + public void evaluateGBDeviceEvent(GBDeviceEvent deviceEvent) { + + } + + @Override + public BluetoothGattCharacteristic getLeCharacteristic(UUID uuid) { + return null; + } + + @Override + public HuaweiPacket.ParamsProvider getParamsProvider() { + return null; + } + + @Override + public void addInProgressRequest(Request request) { + + } + + @Override + public void removeInProgressRequests(Request request) { + + } + + @Override + public void setSecretKey(byte[] authKey) { + + } + + @Override + public byte[] getSecretKey() { + return new byte[0]; + } + + @Override + public void addTotalFitnessData(int steps, int calories, int distance) { + + } + + @Override + public void addSleepActivity(int timestamp, short duration, byte type) { + + } + + @Override + public void addStepData(int timestamp, short steps, short calories, short distance, byte spo, byte heartrate) { + + } + + @Override + public Long addWorkoutTotalsData(Workout.WorkoutTotals.Response packet) { + return null; + } + + @Override + public void addWorkoutSampleData(Long workoutId, List dataList) { + + } + + @Override + public void addWorkoutPaceData(Long workoutId, List paceList) { + + } + + @Override + public void sendSetMusic() { + + } + }; + + @Test + public void emptyPacket() throws Request.RequestCreationException { + DebugRequest debugRequest = new DebugRequest(supportProvider); + + HuaweiPacket expected = new HuaweiPacket(supportProvider.getParamsProvider()); + expected.serviceId = 1; + expected.commandId = 1; + expected.setEncryption(false); + expected.complete = true; + + HuaweiPacket packet = debugRequest.parseDebugString("1,1,false"); + + Assert.assertEquals(expected, packet); + } + + @Test + public void emptyTag() throws Request.RequestCreationException { + DebugRequest debugRequest = new DebugRequest(supportProvider); + + HuaweiPacket expected = new HuaweiPacket(supportProvider.getParamsProvider()); + expected.serviceId = 1; + expected.commandId = 1; + expected.setEncryption(false); + expected.setTlv(new HuaweiTLV().put(1)); + expected.complete = true; + + HuaweiPacket packet = debugRequest.parseDebugString("1,1,false,(1,/)"); + + Assert.assertEquals(expected, packet); + } + + @Test + public void byteTag() throws Request.RequestCreationException { + DebugRequest debugRequest = new DebugRequest(supportProvider); + + HuaweiPacket expected = new HuaweiPacket(supportProvider.getParamsProvider()); + expected.serviceId = 1; + expected.commandId = 1; + expected.setEncryption(false); + expected.setTlv(new HuaweiTLV().put(1, (byte) 1)); + expected.complete = true; + + HuaweiPacket packet = debugRequest.parseDebugString("1,1,false,(1,B1)"); + + Assert.assertEquals(expected, packet); + } + + @Test + public void shortTag() throws Request.RequestCreationException { + DebugRequest debugRequest = new DebugRequest(supportProvider); + + HuaweiPacket expected = new HuaweiPacket(supportProvider.getParamsProvider()); + expected.serviceId = 1; + expected.commandId = 1; + expected.setEncryption(false); + expected.setTlv(new HuaweiTLV().put(1, (short) 1)); + expected.complete = true; + + HuaweiPacket packet = debugRequest.parseDebugString("1,1,false,(1,S1)"); + + Assert.assertEquals(expected, packet); + } + + @Test + public void integerTag() throws Request.RequestCreationException { + DebugRequest debugRequest = new DebugRequest(supportProvider); + + HuaweiPacket expected = new HuaweiPacket(supportProvider.getParamsProvider()); + expected.serviceId = 1; + expected.commandId = 1; + expected.setEncryption(false); + expected.setTlv(new HuaweiTLV().put(1, (int) 1)); + expected.complete = true; + + HuaweiPacket packet = debugRequest.parseDebugString("1,1,false,(1,I1)"); + + Assert.assertEquals(expected, packet); + } + + @Test + public void booleanTag() throws Request.RequestCreationException { + DebugRequest debugRequest = new DebugRequest(supportProvider); + + HuaweiPacket expected = new HuaweiPacket(supportProvider.getParamsProvider()); + expected.serviceId = 1; + expected.commandId = 1; + expected.setEncryption(false); + expected.setTlv(new HuaweiTLV().put(1, true)); + expected.complete = true; + + HuaweiPacket packet = debugRequest.parseDebugString("1,1,false,(1,b1)"); + + Assert.assertEquals(expected, packet); + } + + @Test + public void arrayTag() throws Request.RequestCreationException { + DebugRequest debugRequest = new DebugRequest(supportProvider); + + HuaweiPacket expected = new HuaweiPacket(supportProvider.getParamsProvider()); + expected.serviceId = 1; + expected.commandId = 1; + expected.setEncryption(false); + expected.setTlv(new HuaweiTLV().put(1, new byte[] {(byte) 0xCA, (byte) 0xFE})); + expected.complete = true; + + HuaweiPacket packet = debugRequest.parseDebugString("1,1,false,(1,aCAFE)"); + + Assert.assertEquals(expected, packet); + } + + @Test + public void stringTag() throws Request.RequestCreationException { + DebugRequest debugRequest = new DebugRequest(supportProvider); + + HuaweiPacket expected = new HuaweiPacket(supportProvider.getParamsProvider()); + expected.serviceId = 1; + expected.commandId = 1; + expected.setEncryption(false); + expected.setTlv(new HuaweiTLV().put(1, new byte[] {0x79, 0x65, 0x73})); + expected.complete = true; + + HuaweiPacket packet = debugRequest.parseDebugString("1,1,false,(1,-yes)"); + + Assert.assertEquals(expected, packet); + } + + @Test + public void hexValues() throws Request.RequestCreationException { + DebugRequest debugRequest = new DebugRequest(supportProvider); + + HuaweiPacket expected = new HuaweiPacket(supportProvider.getParamsProvider()); + expected.serviceId = 1; + expected.commandId = 1; + expected.setEncryption(false); + expected.setTlv(new HuaweiTLV().put(1, new byte[] {0x79, 0x65, 0x73})); + expected.complete = true; + + HuaweiPacket packet = debugRequest.parseDebugString("0x01,0x1,false,(0x01,-yes)"); + + Assert.assertEquals(expected, packet); + } + + @Test + public void largeServiceCommand() throws Request.RequestCreationException { + DebugRequest debugRequest = new DebugRequest(supportProvider); + + HuaweiPacket expected = new HuaweiPacket(supportProvider.getParamsProvider()); + expected.serviceId = (byte) 0xff; + expected.commandId = (byte) 255; + expected.setEncryption(false); + expected.complete = true; + + HuaweiPacket packet = debugRequest.parseDebugString("0xff,255,false"); + + Assert.assertEquals(expected, packet); + } + + @Test + public void subTlv() throws Request.RequestCreationException { + DebugRequest debugRequest = new DebugRequest(supportProvider); + + HuaweiPacket expected = new HuaweiPacket(supportProvider.getParamsProvider()); + expected.serviceId = 1; + expected.commandId = 1; + expected.setEncryption(false); + expected.setTlv(new HuaweiTLV() + .put(129, new HuaweiTLV() + .put(1) + .put(2) + )); + expected.complete = true; + + HuaweiPacket packet = debugRequest.parseDebugString("1,1,false,(129,(1,/),(2,/))"); + + Assert.assertEquals(expected, packet); + } + + @Test + public void subSubSubTlv() throws Request.RequestCreationException { + DebugRequest debugRequest = new DebugRequest(supportProvider); + + HuaweiPacket expected = new HuaweiPacket(supportProvider.getParamsProvider()); + expected.serviceId = 1; + expected.commandId = 1; + expected.setEncryption(false); + expected.setTlv(new HuaweiTLV() + .put(129, new HuaweiTLV() + .put(129, new HuaweiTLV() + .put(129, new HuaweiTLV() + .put(1) + ) + ) + )); + expected.complete = true; + + HuaweiPacket packet = debugRequest.parseDebugString("1,1,false,(129,(129,(129,(1,/))))"); + + Assert.assertEquals(expected, packet); + } + + @Test + public void subTlvVCombined() throws Request.RequestCreationException { + DebugRequest debugRequest = new DebugRequest(supportProvider); + + HuaweiPacket expected = new HuaweiPacket(supportProvider.getParamsProvider()); + expected.serviceId = 1; + expected.commandId = 1; + expected.setEncryption(false); + expected.setTlv(new HuaweiTLV() + .put(129, new HuaweiTLV() + .put(1) + .put(2, true) + ) + .put(1, true) + ); + expected.complete = true; + + HuaweiPacket packet = debugRequest.parseDebugString("1,1,false,(129,(1,/),(2,b1)),(1,b1)"); + + Assert.assertEquals(expected, packet); + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/TestResponseManager.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/TestResponseManager.java new file mode 100644 index 000000000..dcc210b60 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/TestResponseManager.java @@ -0,0 +1,451 @@ +/* Copyright (C) 2022-2023 Martin.JM + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.bluetooth.BluetoothGattCharacteristic; +import android.content.Context; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Workout; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.btbr.Transaction; +import nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.Request; + +@RunWith(MockitoJUnitRunner.class) +public class TestResponseManager { + + HuaweiSupportProvider supportProvider = new HuaweiSupportProvider(new HuaweiLESupport()) { + + @Override + public boolean isBLE() { + return true; + } + + @Override + public Context getContext() { + return null; + } + + @Override + public GBDevice getDevice() { + return null; + } + + @Override + public byte[] getSerial() { + return new byte[0]; + } + + @Override + public String getDeviceMac() { + return null; + } + + @Override + public byte[] getMacAddress() { + return new byte[0]; + } + + @Override + public byte[] getAndroidId() { + return new byte[0]; + } + + @Override + public short getNotificationId() { + return 0; + } + + @Override + public TransactionBuilder createBrTransactionBuilder(String taskName) { + return null; + } + + @Override + public nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder createLeTransactionBuilder(String taskName) { + return null; + } + + @Override + public void performConnected(Transaction transaction) throws IOException { + + } + + @Override + public void performConnected(nodomain.freeyourgadget.gadgetbridge.service.btle.Transaction transaction) throws IOException { + + } + + @Override + public void evaluateGBDeviceEvent(GBDeviceEvent deviceEvent) { + + } + + @Override + public BluetoothGattCharacteristic getLeCharacteristic(UUID uuid) { + return null; + } + + @Override + public HuaweiPacket.ParamsProvider getParamsProvider() { + return null; + } + + @Override + public void addInProgressRequest(Request request) { + + } + + @Override + public void removeInProgressRequests(Request request) { + + } + + @Override + public void setSecretKey(byte[] authKey) { + + } + + @Override + public byte[] getSecretKey() { + return new byte[0]; + } + + @Override + public void addTotalFitnessData(int steps, int calories, int distance) { + + } + + @Override + public void addSleepActivity(int timestamp, short duration, byte type) { + + } + + @Override + public void addStepData(int timestamp, short steps, short calories, short distance, byte spo, byte heartrate) { + + } + + @Override + public Long addWorkoutTotalsData(Workout.WorkoutTotals.Response packet) { + return null; + } + + @Override + public void addWorkoutSampleData(Long workoutId, List dataList) { + + } + + @Override + public void addWorkoutPaceData(Long workoutId, List paceList) { + + } + + @Override + public void sendSetMusic() { + + } + }; + + Field handlersField; + Field receivedPacketField; + Field asynchronousResponseField; + + @Before + public void beforeClass() throws NoSuchFieldException { + handlersField = ResponseManager.class.getDeclaredField("handlers"); + handlersField.setAccessible(true); + + asynchronousResponseField = ResponseManager.class.getDeclaredField("asynchronousResponse"); + asynchronousResponseField.setAccessible(true); + + receivedPacketField = ResponseManager.class.getDeclaredField("receivedPacket"); + receivedPacketField.setAccessible(true); + } + + @Test + public void testAddHandler() throws IllegalAccessException { + Request input = new Request(supportProvider); + + List expectedHandlers = Collections.synchronizedList(new ArrayList()); + expectedHandlers.add(input); + + ResponseManager responseManager = new ResponseManager(supportProvider); + responseManager.addHandler(input); + + Assert.assertEquals(expectedHandlers, handlersField.get(responseManager)); + } + + @Test + public void testRemoveHandler() throws IllegalAccessException { + Request input = new Request(supportProvider); + Request extra = new Request(supportProvider); + + List inputHandlers = Collections.synchronizedList(new ArrayList()); + inputHandlers.add(extra); + inputHandlers.add(input); + inputHandlers.add(extra); + + List expectedHandlers = Collections.synchronizedList(new ArrayList()); + expectedHandlers.add(extra); + expectedHandlers.add(extra); + + ResponseManager responseManager = new ResponseManager(supportProvider); + handlersField.set(responseManager, inputHandlers); + + responseManager.removeHandler(input); + + Assert.assertEquals(expectedHandlers, handlersField.get(responseManager)); + } + + @Test + public void testHandleDataCompletePacketSynchronous() throws Exception { + // Note that this is not a proper packet, but that doesn't matter as we're not testing + // the packet parsing. + byte[] input = {0x01, 0x02, 0x03, 0x04}; + + AsynchronousResponse mockAsynchronousResponse = Mockito.mock(AsynchronousResponse.class); + + HuaweiPacket mockHuaweiPacket = Mockito.mock(HuaweiPacket.class); + mockHuaweiPacket.complete = true; + when(mockHuaweiPacket.parse((byte[]) any())) + .thenReturn(mockHuaweiPacket); + + Request request1 = Mockito.mock(Request.class); + when(request1.handleResponse((HuaweiPacket) any())) + .thenReturn(true); + Request request2 = Mockito.mock(Request.class); + // FIXME: Removed due to UnnecessaryStubbingException after mockito-core update + //when(request2.handleResponse((HuaweiPacket) any())) + // .thenReturn(false); + + List inputHandlers = Collections.synchronizedList(new ArrayList()); + inputHandlers.add(request1); + inputHandlers.add(request2); + + List expectedHandlers = Collections.synchronizedList(new ArrayList()); + expectedHandlers.add(request2); + + ResponseManager responseManager = new ResponseManager(supportProvider); + handlersField.set(responseManager, inputHandlers); + receivedPacketField.set(responseManager, mockHuaweiPacket); + asynchronousResponseField.set(responseManager, mockAsynchronousResponse); + + responseManager.handleData(input); + + Assert.assertEquals(expectedHandlers, handlersField.get(responseManager)); + Assert.assertNull(receivedPacketField.get(responseManager)); + + verify(mockHuaweiPacket, times(1)).parse(input); + verify(mockAsynchronousResponse, times(0)).handleResponse((HuaweiPacket) any()); + verify(request1, times(1)).handleResponse(mockHuaweiPacket); + verify(request1, times(1)).handleResponse(); + verify(request2, times(0)).handleResponse((HuaweiPacket) any()); + verify(request2, times(0)).handleResponse(); + } + + @Test + public void testHandleDataCompletePacketAsynchronous() throws Exception { + // Note that this is not a proper packet, but that doesn't matter as we're not testing + // the packet parsing. + byte[] input = {0x01, 0x02, 0x03, 0x04}; + + AsynchronousResponse mockAsynchronousResponse = Mockito.mock(AsynchronousResponse.class); + + HuaweiPacket mockHuaweiPacket = Mockito.mock(HuaweiPacket.class); + mockHuaweiPacket.complete = true; + when(mockHuaweiPacket.parse((byte[]) any())) + .thenReturn(mockHuaweiPacket); + + Request request1 = Mockito.mock(Request.class); + when(request1.handleResponse((HuaweiPacket) any())) + .thenReturn(false); + Request request2 = Mockito.mock(Request.class); + when(request2.handleResponse((HuaweiPacket) any())) + .thenReturn(false); + + List inputHandlers = Collections.synchronizedList(new ArrayList()); + inputHandlers.add(request1); + inputHandlers.add(request2); + + List expectedHandlers = Collections.synchronizedList(new ArrayList()); + expectedHandlers.add(request1); + expectedHandlers.add(request2); + + ResponseManager responseManager = new ResponseManager(supportProvider); + handlersField.set(responseManager, inputHandlers); + receivedPacketField.set(responseManager, mockHuaweiPacket); + asynchronousResponseField.set(responseManager, mockAsynchronousResponse); + + responseManager.handleData(input); + + Assert.assertEquals(expectedHandlers, handlersField.get(responseManager)); + Assert.assertNull(receivedPacketField.get(responseManager)); + + verify(mockHuaweiPacket, times(1)).parse(input); + verify(mockAsynchronousResponse, times(1)).handleResponse(mockHuaweiPacket); + verify(request1, times(1)).handleResponse(mockHuaweiPacket); + verify(request1, times(0)).handleResponse(); + verify(request2, times(1)).handleResponse(mockHuaweiPacket); + verify(request2, times(0)).handleResponse(); + } + + @Test + public void testHandleDataTwoPartialPacketsSynchronous() throws Exception { + // Note that this is not a proper packet, but that doesn't matter as we're not testing + // the packet parsing. + byte[] input1 = {0x01, 0x02, 0x03, 0x04}; + byte[] input2 = {0x05, 0x06, 0x07, 0x08}; + + AsynchronousResponse mockAsynchronousResponse = Mockito.mock(AsynchronousResponse.class); + + HuaweiPacket mockHuaweiPacket = Mockito.mock(HuaweiPacket.class); + mockHuaweiPacket.complete = false; + when(mockHuaweiPacket.parse((byte[]) any())) + .thenReturn(mockHuaweiPacket); + + Request request1 = Mockito.mock(Request.class); + when(request1.handleResponse((HuaweiPacket) any())) + .thenReturn(true); + Request request2 = Mockito.mock(Request.class); + // FIXME: Removed due to UnnecessaryStubbingException after mockito-core update + //when(request2.handleResponse((HuaweiPacket) any())) + // .thenReturn(false); + + List inputHandlers = Collections.synchronizedList(new ArrayList()); + inputHandlers.add(request1); + inputHandlers.add(request2); + + List expectedHandlers1 = Collections.synchronizedList(new ArrayList()); + expectedHandlers1.add(request1); + expectedHandlers1.add(request2); + + List expectedHandlers2 = Collections.synchronizedList(new ArrayList()); + expectedHandlers2.add(request2); + + ResponseManager responseManager = new ResponseManager(supportProvider); + handlersField.set(responseManager, inputHandlers); + receivedPacketField.set(responseManager, mockHuaweiPacket); + asynchronousResponseField.set(responseManager, mockAsynchronousResponse); + + responseManager.handleData(input1); + + Assert.assertEquals(expectedHandlers1, handlersField.get(responseManager)); + Assert.assertEquals(mockHuaweiPacket, receivedPacketField.get(responseManager)); + + verify(mockHuaweiPacket, times(1)).parse(input1); + verify(mockAsynchronousResponse, times(0)).handleResponse((HuaweiPacket) any()); + verify(request1, times(0)).handleResponse(mockHuaweiPacket); + verify(request1, times(0)).handleResponse(); + verify(request2, times(0)).handleResponse((HuaweiPacket) any()); + verify(request2, times(0)).handleResponse(); + + mockHuaweiPacket.complete = true; + responseManager.handleData(input2); + + Assert.assertEquals(expectedHandlers2, handlersField.get(responseManager)); + Assert.assertNull(receivedPacketField.get(responseManager)); + + verify(mockHuaweiPacket, times(1)).parse(input2); + verify(mockAsynchronousResponse, times(0)).handleResponse((HuaweiPacket) any()); + verify(request1, times(1)).handleResponse(mockHuaweiPacket); + verify(request1, times(1)).handleResponse(); + verify(request2, times(0)).handleResponse((HuaweiPacket) any()); + verify(request2, times(0)).handleResponse(); + } + + @Test + public void testHandleDataTwoPartialPacketsAsynchronous() throws Exception { + // Note that this is not a proper packet, but that doesn't matter as we're not testing + // the packet parsing. + byte[] input1 = {0x01, 0x02, 0x03, 0x04}; + byte[] input2 = {0x05, 0x06, 0x07, 0x08}; + + AsynchronousResponse mockAsynchronousResponse = Mockito.mock(AsynchronousResponse.class); + + HuaweiPacket mockHuaweiPacket = Mockito.mock(HuaweiPacket.class); + mockHuaweiPacket.complete = false; + when(mockHuaweiPacket.parse((byte[]) any())) + .thenReturn(mockHuaweiPacket); + + Request request1 = Mockito.mock(Request.class); + when(request1.handleResponse((HuaweiPacket) any())) + .thenReturn(false); + Request request2 = Mockito.mock(Request.class); + when(request2.handleResponse((HuaweiPacket) any())) + .thenReturn(false); + + List inputHandlers = Collections.synchronizedList(new ArrayList()); + inputHandlers.add(request1); + inputHandlers.add(request2); + + List expectedHandlers = Collections.synchronizedList(new ArrayList()); + expectedHandlers.add(request1); + expectedHandlers.add(request2); + + ResponseManager responseManager = new ResponseManager(supportProvider); + handlersField.set(responseManager, inputHandlers); + receivedPacketField.set(responseManager, mockHuaweiPacket); + asynchronousResponseField.set(responseManager, mockAsynchronousResponse); + + responseManager.handleData(input1); + + Assert.assertEquals(expectedHandlers, handlersField.get(responseManager)); + Assert.assertEquals(mockHuaweiPacket, receivedPacketField.get(responseManager)); + + verify(mockHuaweiPacket, times(1)).parse(input1); + verify(mockAsynchronousResponse, times(0)).handleResponse((HuaweiPacket) any()); + verify(request1, times(0)).handleResponse(mockHuaweiPacket); + verify(request1, times(0)).handleResponse(); + verify(request2, times(0)).handleResponse((HuaweiPacket) any()); + verify(request2, times(0)).handleResponse(); + + mockHuaweiPacket.complete = true; + responseManager.handleData(input2); + + Assert.assertEquals(expectedHandlers, handlersField.get(responseManager)); + Assert.assertNull(receivedPacketField.get(responseManager)); + + verify(mockHuaweiPacket, times(1)).parse(input2); + verify(mockAsynchronousResponse, times(1)).handleResponse((HuaweiPacket) any()); + verify(request1, times(1)).handleResponse(mockHuaweiPacket); + verify(request1, times(0)).handleResponse(); + verify(request2, times(1)).handleResponse((HuaweiPacket) any()); + verify(request2, times(0)).handleResponse(); + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityFileIdTest.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityFileIdTest.java new file mode 100644 index 000000000..f214d0f10 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityFileIdTest.java @@ -0,0 +1,83 @@ +/* Copyright (C) 2023 JosÊ Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class XiaomiActivityFileIdTest { + @Test + public void testEncode() { + final byte[] expectedEncoding = GB.hexStringToByteArray("21F3286504008C"); + final XiaomiActivityFileId xiaomiActivityFileId = new XiaomiActivityFileId( + new Date(1697182497000L), + 4, + 3, + 1, + 8, + 0 + ); + + assertArrayEquals(expectedEncoding, xiaomiActivityFileId.toBytes()); + } + + @Test + public void testDecode() { + final byte[] bytes = GB.hexStringToByteArray("21F328650403A0"); + final XiaomiActivityFileId expectedFileId = XiaomiActivityFileId.from(bytes); + + assertEquals(1697182497000L, expectedFileId.getTimestamp().getTime()); + assertEquals(4, expectedFileId.getTimezone()); + assertEquals(3, expectedFileId.getVersion()); + assertEquals(XiaomiActivityFileId.Type.SPORTS, expectedFileId.getType()); + assertEquals(XiaomiActivityFileId.Subtype.SPORTS_FREESTYLE, expectedFileId.getSubtype()); + assertEquals(XiaomiActivityFileId.DetailType.DETAILS, expectedFileId.getDetailType()); + } + + @Test + public void testDecodeEncode() { + final byte[] bytes = GB.hexStringToByteArray("21F328650403A021F3286504008C"); + + final ByteBuffer bufDecode = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); + + final List fileIds = new ArrayList<>(); + + while (bufDecode.position() < bufDecode.limit()) { + final XiaomiActivityFileId fileId = XiaomiActivityFileId.from(bufDecode); + fileIds.add(fileId); + System.out.println(fileId); + } + + final ByteBuffer bufEncode = ByteBuffer.allocate(fileIds.size() * 7).order(ByteOrder.LITTLE_ENDIAN); + + for (final XiaomiActivityFileId fileId : fileIds) { + bufEncode.put(fileId.toBytes()); + } + + assertArrayEquals(bytes, bufEncode.array()); + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/ZipFileTest.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/GBZipFileTest.java similarity index 93% rename from app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/ZipFileTest.java rename to app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/GBZipFileTest.java index e9934b135..d634643d6 100644 --- a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/ZipFileTest.java +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/GBZipFileTest.java @@ -12,10 +12,10 @@ import java.nio.charset.StandardCharsets; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; -import nodomain.freeyourgadget.gadgetbridge.util.ZipFile; +import nodomain.freeyourgadget.gadgetbridge.util.GBZipFile; import nodomain.freeyourgadget.gadgetbridge.util.ZipFileException; -public class ZipFileTest extends TestBase { +public class GBZipFileTest extends TestBase { private static final String TEST_FILE_NAME = "manifest.json"; private static final String TEST_NESTED_FILE_NAME = "directory/manifest.json"; private static final String TEST_FILE_CONTENTS_1 = "{ \"mykey\": \"myvalue\", \"myarr\": [0, 1, 2, 3] }"; @@ -44,7 +44,7 @@ public class ZipFileTest extends TestBase { byte[] zipArchive = createZipArchive(TEST_FILE_NAME, contents); - ZipFile zipFile = new ZipFile(zipArchive); + GBZipFile zipFile = new GBZipFile(zipArchive); String readContents = new String(zipFile.getFileFromZip(TEST_FILE_NAME)); Assert.assertEquals(contents, readContents); @@ -56,7 +56,7 @@ public class ZipFileTest extends TestBase { byte[] zipArchive = createZipArchive(TEST_FILE_NAME, contents); - ZipFile zipFile = new ZipFile(zipArchive); + GBZipFile zipFile = new GBZipFile(zipArchive); String readContents = new String(zipFile.getFileFromZip(TEST_FILE_NAME)); Assert.assertEquals(contents, readContents); @@ -68,7 +68,7 @@ public class ZipFileTest extends TestBase { byte[] zipArchive = createZipArchive(TEST_FILE_NAME, contents); - ZipFile zipFile = new ZipFile(zipArchive); + GBZipFile zipFile = new GBZipFile(zipArchive); String readContents = new String(zipFile.getFileFromZip(TEST_FILE_NAME)); Assert.assertEquals(contents, readContents); @@ -80,7 +80,7 @@ public class ZipFileTest extends TestBase { byte[] zipArchive = createZipArchive(TEST_FILE_NAME, contents); - ZipFile zipFile = new ZipFile(zipArchive); + GBZipFile zipFile = new GBZipFile(zipArchive); String readContents = new String(zipFile.getFileFromZip(TEST_FILE_NAME)); Assert.assertEquals(contents, readContents); @@ -92,7 +92,7 @@ public class ZipFileTest extends TestBase { byte[] zipArchive = createZipArchive(TEST_NESTED_FILE_NAME, contents); - ZipFile zipFile = new ZipFile(zipArchive); + GBZipFile zipFile = new GBZipFile(zipArchive); String readContents = new String(zipFile.getFileFromZip(TEST_NESTED_FILE_NAME)); Assert.assertEquals(contents, readContents); @@ -112,7 +112,7 @@ public class ZipFileTest extends TestBase { writeFileToZip(contents3, "file3", zipWriteStream); zipWriteStream.close(); - ZipFile zipFile = new ZipFile(baos.toByteArray()); + GBZipFile zipFile = new GBZipFile(baos.toByteArray()); String readContents2 = new String(zipFile.getFileFromZip("file2")); String readContents1 = new String(zipFile.getFileFromZip("file1")); String readContents3 = new String(zipFile.getFileFromZip("file3")); @@ -132,7 +132,7 @@ public class ZipFileTest extends TestBase { writeFileToZip("Hello, World!", "folder1/file3", zipWriteStream); zipWriteStream.close(); - final ZipFile zipFile = new ZipFile(baos.toByteArray()); + final GBZipFile zipFile = new GBZipFile(baos.toByteArray()); Assert.assertTrue(zipFile.fileExists("file2")); Assert.assertTrue(zipFile.fileExists("file1")); Assert.assertTrue(zipFile.fileExists("folder1/file3")); diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/util/language/LanguageUtilsTest.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/util/language/LanguageUtilsTest.java index 0f25a5244..08a359860 100644 --- a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/util/language/LanguageUtilsTest.java +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/util/language/LanguageUtilsTest.java @@ -4,9 +4,13 @@ import android.content.SharedPreferences; import org.junit.Test; +import java.util.Arrays; + import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.test.TestBase; +import nodomain.freeyourgadget.gadgetbridge.util.language.impl.CzechTransliterator; +import nodomain.freeyourgadget.gadgetbridge.util.language.impl.ExtendedAsciiTransliterator; import nodomain.freeyourgadget.gadgetbridge.util.language.impl.FlattenToAsciiTransliterator; import static org.junit.Assert.assertEquals; @@ -96,9 +100,9 @@ public class LanguageUtilsTest extends TestBase { final Transliterator transliterator = LanguageUtils.getTransliterator("bengali"); // input with cyrillic and diacritic letters - String[] inputs = { "āĻ…āĻ¨āĻŋāĻ°ā§āĻĻā§āĻ§", "āĻŦāĻŋāĻœā§āĻžāĻžāĻ¨āĻ¯āĻžāĻ¤ā§āĻ°āĻž āĻšāĻ˛āĻ›ā§‡ āĻšāĻ˛āĻŦā§‡āĨ¤", "āĻ†āĻŽāĻŋ āĻ¸āĻŦ āĻĻā§‡āĻ–ā§‡āĻļā§āĻ¨ā§‡ āĻ•ā§āĻˇā§‡āĻĒā§‡ āĻ—āĻŋā§Ÿā§‡ āĻ•āĻ°āĻŋ āĻŦāĻžāĻ™āĻ˛āĻžā§Ÿ āĻšāĻŋā§ŽāĻ•āĻžāĻ°!", - "āĻ†āĻŽāĻžāĻ° āĻœāĻžāĻ­āĻž āĻ•ā§‹āĻĄ is so bad! āĻ•ā§€ āĻ†āĻ° āĻŦāĻ˛āĻŦā§‹!" }; - String[] outputs = { "oniruddho", "biggaanJaatraa cholchhe cholbe.", + String[] inputs = {"āĻ…āĻ¨āĻŋāĻ°ā§āĻĻā§āĻ§", "āĻŦāĻŋāĻœā§āĻžāĻžāĻ¨āĻ¯āĻžāĻ¤ā§āĻ°āĻž āĻšāĻ˛āĻ›ā§‡ āĻšāĻ˛āĻŦā§‡āĨ¤", "āĻ†āĻŽāĻŋ āĻ¸āĻŦ āĻĻā§‡āĻ–ā§‡āĻļā§āĻ¨ā§‡ āĻ•ā§āĻˇā§‡āĻĒā§‡ āĻ—āĻŋā§Ÿā§‡ āĻ•āĻ°āĻŋ āĻŦāĻžāĻ™āĻ˛āĻžā§Ÿ āĻšāĻŋā§ŽāĻ•āĻžāĻ°!", + "āĻ†āĻŽāĻžāĻ° āĻœāĻžāĻ­āĻž āĻ•ā§‹āĻĄ is so bad! āĻ•ā§€ āĻ†āĻ° āĻŦāĻ˛āĻŦā§‹!"}; + String[] outputs = {"oniruddho", "biggaanJaatraa cholchhe cholbe.", "aami sob dekheshune kkhepe giye kori baanglaay chitkaar!", "aamaar jaabhaa koD is so bad! kii aar bolbo!"}; @@ -189,6 +193,16 @@ public class LanguageUtilsTest extends TestBase { assertEquals("georgian transliteration failed", expected, output); } + @Test + public void testStringTransliterateHungarian() { + final Transliterator transliterator = LanguageUtils.getTransliterator("hungarian"); + + String input = "ÃĄ Ê í Ãŗ Ãļ ő Ãŧ Åą"; + String output = transliterator.transliterate(input); + String expected = "a e i o o o u u"; + assertEquals("hungarian transliteration failed", expected, output); + } + @Test public void testStringTransliterateCommonSymbols() { final Transliterator transliterator = LanguageUtils.getTransliterator("common_symbols"); @@ -217,12 +231,23 @@ public class LanguageUtilsTest extends TestBase { @Test public void testFlattenToAscii() throws Exception { final FlattenToAsciiTransliterator transliterator = new FlattenToAsciiTransliterator(); - String input = "ä ș ț ă"; + String input = "ä ș ț ă īŦne"; String output = transliterator.transliterate(input); - String expected = "a s t a"; + String expected = "a s t a fine"; assertEquals("flatten to ascii transliteration failed", expected, output); } + @Test + public void testMultitransliterator() throws Exception { + final MultiTransliterator multiTransliterator = new MultiTransliterator(Arrays.asList( + new CzechTransliterator(), + new ExtendedAsciiTransliterator(), + new FlattenToAsciiTransliterator() + )); + assertEquals("Zlutoucky kun upel \"dabelske\" \"ody\"", multiTransliterator.transliterate("ÅŊluÅĨoučkÃŊ kůň Ãēpěl ÂģÄÃĄbelskÊÂĢ „Ãŗdy“")); + assertEquals("300 Kc", multiTransliterator.transliterate("300\u00A0Kč")); + } + @Test public void testTransliterateOption() throws Exception { enableTransliteration(false); diff --git a/build.gradle b/build.gradle index e09271d1c..ed86e9363 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:7.1.3' + classpath 'com.android.tools.build:gradle:7.4.2' classpath 'gradle.plugin.com.github.spotbugs:spotbugs-gradle-plugin:2.0.0' classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.17' diff --git a/external/fossil-hr-gbapps b/external/fossil-hr-gbapps index 0d8312b39..3c9007742 160000 --- a/external/fossil-hr-gbapps +++ b/external/fossil-hr-gbapps @@ -1 +1 @@ -Subproject commit 0d8312b39771e08aa7bd1a23c114beaffae0ef11 +Subproject commit 3c900774207d9dd904886433d672d22d5bd0dea4 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7454180f2..943f0cbfa 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a2e01c0df..070cb702f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=f581709a9c35e9cb92e16f585d2c4bc99b2b1a5f85d2badbd3dc6bff59e1e6dd -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 1b6c78733..65dcd68d6 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,10 +80,10 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' @@ -143,12 +143,16 @@ fi if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -205,6 +209,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index ac1b06f93..6689b85be 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/src/main/fastlane/metadata/android/en-US/changelogs/227.txt b/src/main/fastlane/metadata/android/en-US/changelogs/227.txt new file mode 100644 index 000000000..9c20dcb28 --- /dev/null +++ b/src/main/fastlane/metadata/android/en-US/changelogs/227.txt @@ -0,0 +1,78 @@ +* Initial support for Amazfit Balance +* Initial support for Amazfit Active +* Initial support for ColaCao 2021 +* Initial support for ColaCao 2023 +* Initial support for Femometer Vinca II +* Initial support for Mijia LYWSD02MMC variant +* Initial support for Sony Wena 3 +* Experimental support for Divoom Pixoo +* Experimental support for Sony WF-1000XM5 +* Experimental support for Amazfit Active Edge +* Experimental support for Mi Band 7 Pro (Xiaomi Smart Band 7 Pro) +* Experimental support for Mi Band 8 (Xiaomi Smart Band 8) +* Experimental support for Mi Watch Lite +* Experimental support for Mi Watch Color Sport +* Experimental support for Redmi Smart Band 2 +* Experimental support for Redmi Watch 3 Active +* Experimental support for Xiaomi Watch S1 Active +* Amazfit Band 7: Add alexa menu entries +* Amazfit GTR 3 Pro: Fix firmware and watchface upload +* Amazfit T-Rex: Fix activity summary parsing +* Amazfit T-Rex Pro: Add activate display on lift sensitivity +* AsteroidOS: Add more supported watch models +* AsteroidOS: Fix media info +* AsteroidOS: Fix notification dismissal +* Bangle.js: Add loyalty cards integration with Catima +* Bangle.js: Ensure SMS messages have src field set to "SMS Message" +* Bangle.js: Fix GPS speed +* Bangle.js: Improve handling of chinese characters +* Bangle.js: Lower threshold for low battery warning +* Bangle.js: Recover from device initialization failure +* Casio GBX100/GBD-200: Fix first connect +* Casio GB5600/6900/STB-1000: Fix pairing +* Casio GDB-200: Fix notification timestamp +* Casio GDB-200: Fixed notification categories and default category +* Casio GDB-200: Allow preview of notification message alongside title +* Casio GDB-200: Fixed find my phone feature +* Intent API: Add debug action for test new function +* Fossil/Skagen Hybrids: Add new navigation app +* Fossil/Skagen Hybrids: Allow configuring call rejection method +* Fossil/Skagen Hybrids: Fix some preference crashes on the nightly +* Fossil/Skagen Hybrids: Reduce toasts on release builds +* Fossil/Skagen Hybrids: Show device specific settings in more logical order +* Huami: Toggle phone silent mode from band +* Message privacy: Add mode Hide only body +* Mijia LYWSD02: Add battery +* Mijia LYWSD02: Add low battery notification +* Mijia LYWSD02: Set temperature unit +* Mijia LYWSD02: Fix battery drain while connected +* PineTime: Display app name for VoIP app calls +* PineTime: Honor Sync time setting on connect +* PineTime: Improve notification handling +* PineTime: Reduce weather memory usage +* Withings Steel HR: Fix crash when calibrating hands on the nightly +* Zepp OS: Add blood oxygen graph +* Zepp OS: Add workout codes for hiking and outdoor swimming +* Zepp OS: Allow disabling app notifications per device +* Zepp OS: Attempt to fix activity fetch operation getting stuck +* Zepp OS: Display swimming activity data +* Zepp OS: Fix health settings on older Zepp OS versions +* Zepp OS: Fix setting of unknown button press apps +* Zepp OS: Fix sunrise and moon dates being off by local time + UTC offset +* Zepp OS: Map hiking, outdoor swimming, climbing and table tennis activity types +* Zepp OS: Toggle phone silent mode from band +* Add transliteration for Latvian, Hungarian, Common Symbols +* Allow multiple device actions to be triggered for the same event +* Allow toggling DND through device actions +* Autodetect OsmAnd package name and make it configurable +* Improve ASCII transliterator +* Make GMaps navigation handler follow the "navigation forwarding" setting +* Support selecting enabled navigation apps +* Allow ignore notifications from work profile apps +* Display alias in low battery notification +* Fix crash when pairing current device as companion +* Fix emoji when a transliterator is enabled +* Fix UV Index and rain probability for some weather apps +* Improve device discovery stability and fix freezes +* Improve Telegram and COL Reminder notifications +* Replace old-style preference switch with Material 3 switch diff --git a/src/main/fastlane/metadata/android/en-US/changelogs/228.txt b/src/main/fastlane/metadata/android/en-US/changelogs/228.txt new file mode 100644 index 000000000..d818f9535 --- /dev/null +++ b/src/main/fastlane/metadata/android/en-US/changelogs/228.txt @@ -0,0 +1,28 @@ +* Initial support for Honor Band 3,4,5,6 +* Initial support for Huawei Band 4, 4 Pro, 6, 7, 3e, 4e +* Initial support for Huawei Talk Band B6 +* Initial support for Huawei Watch GT, GT 2 +* Initial support for Mijia LYWSD03MMC +* Initial support for Nothing Ear (2) +* Initial support for Nothing Ear (Stick) +* Experimental support for Honor Band 7 +* Experimental support for Redmi Watch 2 Lite +* Experimental support for Redmi Smart Band Pro +* Casio GBX100: Add support for snooze alarm +* Fossil/Skagen Hybrids: Update navigationApp to 1.1 +* Huami: Fetch SpO2 on devices that support it +* Pebble: Attempt to fix app configuration webview +* PineTime: Add support for InfiniTime's new simple weather +* PineTime: Fix freeze and reboot when upgrading firmware +* Pixoo: Enable sending images (non-persistent) +* Pixoo: Get and send alarms +* Pixoo: Set custom device name +* Pixoo: support "clap hands to turn off screen" and "sleep after silence" settings +* Xiaomi: Improve activity and workout parsing +* Xiaomi: Improve stability and fix some crashes +* Xiaomi: Improve weather +* Xiaomi: Parse sleep stages +* Add a notifications channel for connection status notifications +* Improve automatic connection to all or previous devices +* Fix devices sometimes staying stuck in a "Connecting" state +* Map some missing Google Maps navigation actions diff --git a/src/main/fastlane/metadata/android/en-US/changelogs/229.txt b/src/main/fastlane/metadata/android/en-US/changelogs/229.txt new file mode 100644 index 000000000..fb398599b --- /dev/null +++ b/src/main/fastlane/metadata/android/en-US/changelogs/229.txt @@ -0,0 +1,23 @@ +* Initial support for Honor Magic Watch 2 +* Initial support for Mijia MHO-C303 +* Initial support for Nothing CMF Watch Pro +* Initial support for Sony WI-SP600N +* Experimental support for Redmi Watch 2 +* Experimental support for Xiaomi Smart Band 8 Pro +* Experimental support for Xiaomi Watch S1 Pro +* Experimental support for Xiaomi Watch S1 +* Experimental support for Xiaomi Watch S3 +* Galaxy Buds2 Pro: Fix recognition of some versions +* Huawei Watch GT 2: Fix pairing +* Redmi Smart Band Pro: Fix password digits +* Pebble: Fix app configuration page +* Pebble 2: Fix pairing issue +* PineTime: Fix weather forecast on InfiniTime's new simple weather +* Xiaomi: Fix sleep sometimes extending past the wakeup time +* Xiaomi: Request battery level and charging state periodically +* Xiaomi: Fix sleep stage parsing for some devices +* Zepp OS: Improve device discovery +* Zepp OS: Fix weather not working on some devices +* Zepp OS: Prevent crash when installing large firmware updates +* Fix sport activity summary group order +* Fix reconnection to devices failing occasionally